<route lang="jsonc" type="page">
|
{
|
"layout": "default",
|
"style": {
|
"navigationBarTitleText": "记录详情",
|
"navigationStyle": "custom"
|
}
|
}
|
</route>
|
|
<script lang="ts" setup>
|
import type { ParentHistoryChat, ParentHistoryDetail } from '@/api/parent'
|
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
import { computed, ref } from 'vue'
|
import { getAudioId } from '@/api/chat-history/chat-history'
|
import { getParentHistoryDetail } from '@/api/parent'
|
import { t } from '@/i18n'
|
import { debounce, getEnvBaseUrl } from '@/utils'
|
import { toast } from '@/utils/toast'
|
import { durationMsOf, formatDuration, formatStartedAt, sourceLabel, statusLabel } from './historyText'
|
|
defineOptions({
|
name: 'ParentHistoryDetail',
|
})
|
|
const instanceId = ref('')
|
const agentId = ref('')
|
const macAddress = ref('')
|
const loading = ref(false)
|
const detail = ref<ParentHistoryDetail | null>(null)
|
const audioContext = ref<UniApp.InnerAudioContext | null>(null)
|
const playingAudioId = ref<string | null>(null)
|
|
const headerMeta = computed(() => {
|
if (!detail.value)
|
return ''
|
return [
|
formatStartedAt(detail.value.startedAt),
|
formatDuration(durationMsOf(detail.value), t),
|
sourceLabel(detail.value.source, t),
|
].filter(Boolean).join(' · ')
|
})
|
|
function goBack() {
|
uni.navigateBack()
|
}
|
|
function parseUserMessage(content: string) {
|
try {
|
return JSON.parse(content)
|
}
|
catch {
|
return null
|
}
|
}
|
|
function chatText(message: ParentHistoryChat): string {
|
if (message.chatType === 1) {
|
const parsed = parseUserMessage(message.content)
|
return parsed?.content || message.content
|
}
|
return message.content
|
}
|
|
async function loadDetail() {
|
if (!instanceId.value || !agentId.value || !macAddress.value)
|
return
|
loading.value = true
|
try {
|
detail.value = await getParentHistoryDetail(instanceId.value, macAddress.value, agentId.value)
|
}
|
catch (error) {
|
console.error('load parent history detail failed', error)
|
toast.error(t('parentHistory.loadFailed'))
|
detail.value = null
|
}
|
finally {
|
loading.value = false
|
}
|
}
|
|
function destroyAudio() {
|
const ctx = audioContext.value
|
audioContext.value = null
|
playingAudioId.value = null
|
if (!ctx)
|
return
|
ctx.onEnded(() => {})
|
ctx.onError(() => {})
|
try {
|
ctx.stop()
|
}
|
catch {
|
// already stopped
|
}
|
ctx.destroy()
|
}
|
|
const playAudio = debounce(async (audioId: string) => {
|
if (!audioId) {
|
toast.error(t('parentHistory.noAudio'))
|
return
|
}
|
try {
|
if (playingAudioId.value === audioId) {
|
destroyAudio()
|
return
|
}
|
destroyAudio()
|
const downloadId = await getAudioId(audioId)
|
const audioUrl = `${getEnvBaseUrl()}/agent/play/${downloadId}`
|
audioContext.value = uni.createInnerAudioContext()
|
audioContext.value.src = audioUrl
|
playingAudioId.value = audioId
|
audioContext.value.onEnded(() => {
|
destroyAudio()
|
})
|
audioContext.value.onError(() => {
|
toast.error(t('chatHistory.audioPlayFailed'))
|
destroyAudio()
|
})
|
audioContext.value.play()
|
}
|
catch (error) {
|
console.error('play history audio failed', error)
|
toast.error(t('chatHistory.playAudioFailed'))
|
destroyAudio()
|
}
|
}, 400)
|
|
onLoad((options) => {
|
if (options?.id)
|
instanceId.value = String(options.id)
|
if (options?.agentId)
|
agentId.value = String(options.agentId)
|
if (options?.macAddress)
|
macAddress.value = decodeURIComponent(String(options.macAddress))
|
loadDetail()
|
})
|
|
onUnload(() => {
|
destroyAudio()
|
})
|
</script>
|
|
<template>
|
<view class="history-detail-page">
|
<wd-navbar :title="t('parentHistory.detailTitle')" safe-area-inset-top>
|
<template #left>
|
<wd-icon name="arrow-left" size="18" @click="goBack" />
|
</template>
|
</wd-navbar>
|
|
<view v-if="loading && !detail" class="flex flex-col items-center py-[80rpx]">
|
<wd-loading color="#336cff" />
|
<text class="mt-[16rpx] text-[28rpx] text-[#666]">
|
{{ t('common.loading') }}
|
</text>
|
</view>
|
|
<view v-else-if="detail" class="px-[20rpx] py-[16rpx] pb-[40rpx]">
|
<view class="mb-[16rpx] rounded-[20rpx] bg-white p-[28rpx]">
|
<view class="mb-[8rpx] flex items-center justify-between">
|
<text class="text-[32rpx] text-[#232338] font-semibold">
|
{{ detail.sceneName || detail.scene }}
|
</text>
|
<text
|
class="rounded-[8rpx] px-[12rpx] py-[4rpx] text-[22rpx] text-white"
|
:class="detail.status === 'completed' ? 'bg-[#2ba471]' : detail.status === 'running' ? 'bg-[#336cff]' : 'bg-[#9d9ea3]'"
|
>
|
{{ statusLabel(detail, t) }}
|
</text>
|
</view>
|
<text class="text-[26rpx] text-[#65686f]">
|
{{ headerMeta }}
|
</text>
|
</view>
|
|
<view v-if="detail.kind === 'extensive'" class="rounded-[20rpx] bg-white p-[28rpx]">
|
<text class="block text-[28rpx] leading-[1.6] text-[#65686f]">
|
{{ t('parentHistory.extensiveHint') }}
|
</text>
|
</view>
|
|
<view v-else-if="detail.kind === 'shadowing'">
|
<view v-if="detail.turns.length === 0" class="rounded-[20rpx] bg-white p-[48rpx] text-center text-[26rpx] text-[#999]">
|
{{ t('parentHistory.emptyTurns') }}
|
</view>
|
<view
|
v-for="turn in detail.turns"
|
:key="turn.index"
|
class="mb-[16rpx] rounded-[20rpx] border-[1rpx] border-[#eeeeee] bg-white p-[28rpx]"
|
>
|
<view class="mb-[12rpx] flex items-center justify-between">
|
<text class="text-[24rpx] text-[#9d9ea3]">
|
{{ t('parentHistory.sentence', { n: (turn.index ?? 0) + 1 }) }}
|
</text>
|
<view
|
v-if="turn.audioId"
|
class="text-[#336cff]"
|
@click="playAudio(turn.audioId)"
|
>
|
<wd-icon
|
:name="playingAudioId === turn.audioId ? 'pause-circle-filled' : 'play-circle-filled'"
|
size="22"
|
/>
|
</view>
|
</view>
|
<text class="mb-[8rpx] block text-[24rpx] text-[#9d9ea3]">
|
{{ t('parentHistory.expected') }}
|
</text>
|
<text class="mb-[16rpx] block text-[28rpx] text-[#232338]">
|
{{ turn.expectedText || t('parentHistory.noExpected') }}
|
</text>
|
<text class="mb-[8rpx] block text-[24rpx] text-[#9d9ea3]">
|
{{ t('parentHistory.asr') }}
|
</text>
|
<text class="block text-[28rpx] text-[#232338]">
|
{{ turn.asrText || t('parentHistory.noAsr') }}
|
</text>
|
</view>
|
</view>
|
|
<view v-else>
|
<view v-if="!detail.chats || detail.chats.length === 0" class="rounded-[20rpx] bg-white p-[48rpx] text-center text-[26rpx] text-[#999]">
|
{{ t('parentHistory.noChat') }}
|
</view>
|
<view v-else class="flex flex-col gap-[20rpx]">
|
<view
|
v-for="(message, index) in detail.chats"
|
:key="index"
|
class="w-full flex"
|
:class="message.chatType === 1 ? 'justify-end' : 'justify-start'"
|
>
|
<view
|
class="max-w-[80%] rounded-[20rpx] p-[24rpx]"
|
:class="message.chatType === 1 ? 'bg-[#336cff] text-white' : 'border-[1rpx] border-[#eeeeee] bg-white text-[#232338]'"
|
>
|
<view class="flex items-center gap-[12rpx]">
|
<view
|
v-if="message.audioId"
|
class="flex-shrink-0"
|
:class="message.chatType === 1 ? 'text-white' : 'text-[#336cff]'"
|
@click="playAudio(message.audioId)"
|
>
|
<wd-icon
|
:name="playingAudioId === message.audioId ? 'pause-circle-filled' : 'play-circle-filled'"
|
size="20"
|
/>
|
</view>
|
<text class="text-[28rpx] leading-[1.4]">
|
{{ chatText(message) }}
|
</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
</template>
|
|
<style scoped>
|
.history-detail-page {
|
min-height: 100%;
|
background: #f5f7fb;
|
}
|
</style>
|