feat: 小程序脚本首页重构 + 社交数据导入 + TTS 播放优化
- 后端:新增社交数据导入/审批/洞察生成 API(SocialContent/SocialInsight) - 后端:优化脚本上下文服务,TTS 服务增强 - 小程序:重构脚本首页布局,新增社交导入页面 - 小程序:新增 useTtsPlayer composable,移除旧 ScriptAudioPlayer 组件 - 小程序:新增社交导入服务,优化请求服务 - SQL:新增社交数据导入建表脚本 - 文档:补充设计文档和实施计划 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { createTtsTask, getTtsTask, getTtsTaskBySource } from '../services/tts.js'
|
||||
import analytics from '../services/analytics.js'
|
||||
|
||||
const readResponseData = (response) => response?.data ?? response ?? null
|
||||
|
||||
export const useTtsPlayer = ({ pagePath = '', sourceType = 'epic_script' } = {}) => {
|
||||
const task = ref(null)
|
||||
const loading = ref(false)
|
||||
const playing = ref(false)
|
||||
const statusText = ref('')
|
||||
let audio = null
|
||||
let timer = null
|
||||
|
||||
const buttonText = computed(() => {
|
||||
if (loading.value) return '正在生成朗读'
|
||||
if (task.value?.status === 'success') return playing.value ? '暂停朗读' : '播放朗读'
|
||||
if (task.value?.status === 'failed') return '重试朗读'
|
||||
return '语音播放'
|
||||
})
|
||||
|
||||
const clearTimer = () => {
|
||||
if (timer) clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
|
||||
const stopAudio = () => {
|
||||
if (!audio) return
|
||||
audio.stop()
|
||||
audio.destroy()
|
||||
audio = null
|
||||
playing.value = false
|
||||
}
|
||||
|
||||
const setTask = (nextTask) => {
|
||||
task.value = nextTask
|
||||
if (audio && nextTask?.audioUrl && audio.src !== nextTask.audioUrl) {
|
||||
stopAudio()
|
||||
}
|
||||
}
|
||||
|
||||
const track = (eventName, sourceId, payload = {}) => {
|
||||
analytics.track(eventName, {
|
||||
source_id: sourceId || '',
|
||||
task_id: task.value?.id || '',
|
||||
...payload
|
||||
}, { eventType: 'tts', pagePath })
|
||||
}
|
||||
|
||||
const markFailed = (message) => {
|
||||
loading.value = false
|
||||
statusText.value = message || '朗读暂时不可用'
|
||||
uni.showToast({ title: statusText.value, icon: 'none' })
|
||||
}
|
||||
|
||||
const pollTask = (taskId, sourceId) => {
|
||||
clearTimer()
|
||||
timer = setInterval(async () => {
|
||||
try {
|
||||
const response = await getTtsTask(taskId)
|
||||
const nextTask = readResponseData(response)
|
||||
setTask(nextTask)
|
||||
|
||||
if (nextTask?.status === 'success') {
|
||||
loading.value = false
|
||||
statusText.value = ''
|
||||
clearTimer()
|
||||
track('script_tts_success', sourceId, { task_id: nextTask.id })
|
||||
play(sourceId)
|
||||
return
|
||||
}
|
||||
|
||||
if (nextTask?.status === 'failed') {
|
||||
clearTimer()
|
||||
track('script_tts_error', sourceId, { error: nextTask.errorMessage || '' })
|
||||
markFailed(nextTask.errorMessage || '朗读生成失败')
|
||||
}
|
||||
} catch (error) {
|
||||
clearTimer()
|
||||
track('script_tts_error', sourceId, { error: error?.message || error?.errMsg || 'poll failed' })
|
||||
markFailed('朗读状态获取失败')
|
||||
}
|
||||
}, 2500)
|
||||
}
|
||||
|
||||
const play = (sourceId) => {
|
||||
if (!task.value?.audioUrl) return
|
||||
|
||||
if (!audio) {
|
||||
audio = uni.createInnerAudioContext()
|
||||
audio.src = task.value.audioUrl
|
||||
audio.autoplay = false
|
||||
|
||||
audio.onPlay(() => {
|
||||
playing.value = true
|
||||
track('script_tts_play', sourceId)
|
||||
})
|
||||
|
||||
audio.onPause(() => {
|
||||
playing.value = false
|
||||
track('script_tts_pause', sourceId)
|
||||
})
|
||||
|
||||
audio.onEnded(() => {
|
||||
playing.value = false
|
||||
track('script_tts_complete', sourceId)
|
||||
})
|
||||
|
||||
audio.onError((error) => {
|
||||
playing.value = false
|
||||
track('script_tts_error', sourceId, { error: error?.errMsg || 'play failed' })
|
||||
markFailed('音频播放失败')
|
||||
})
|
||||
}
|
||||
|
||||
if (playing.value) {
|
||||
audio.pause()
|
||||
} else {
|
||||
audio.play()
|
||||
}
|
||||
}
|
||||
|
||||
const createAndPoll = async (sourceId) => {
|
||||
loading.value = true
|
||||
statusText.value = ''
|
||||
track('script_tts_request', sourceId)
|
||||
|
||||
try {
|
||||
const response = await createTtsTask({ sourceType, sourceId })
|
||||
const nextTask = readResponseData(response)
|
||||
setTask(nextTask)
|
||||
|
||||
if (nextTask?.status === 'success') {
|
||||
loading.value = false
|
||||
track('script_tts_success', sourceId, { task_id: nextTask.id })
|
||||
play(sourceId)
|
||||
return
|
||||
}
|
||||
|
||||
if (nextTask?.status === 'failed') {
|
||||
track('script_tts_error', sourceId, { error: nextTask.errorMessage || '' })
|
||||
markFailed(nextTask.errorMessage || '朗读生成失败')
|
||||
return
|
||||
}
|
||||
|
||||
if (nextTask?.id) {
|
||||
pollTask(nextTask.id, sourceId)
|
||||
return
|
||||
}
|
||||
|
||||
markFailed('朗读任务创建失败')
|
||||
} catch (error) {
|
||||
track('script_tts_error', sourceId, { error: error?.message || error?.errMsg || 'create failed' })
|
||||
markFailed(error?.message || '朗读任务创建失败')
|
||||
}
|
||||
}
|
||||
|
||||
const playSource = async (sourceId) => {
|
||||
if (!sourceId) {
|
||||
uni.showToast({ title: '生成保存后可播放', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (loading.value) return
|
||||
|
||||
if (task.value?.status === 'success') {
|
||||
play(sourceId)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getTtsTaskBySource({ sourceType, sourceId })
|
||||
const existingTask = readResponseData(response)
|
||||
setTask(existingTask)
|
||||
|
||||
if (existingTask?.status === 'success') {
|
||||
play(sourceId)
|
||||
return
|
||||
}
|
||||
|
||||
if (existingTask?.status === 'pending' || existingTask?.status === 'processing') {
|
||||
loading.value = true
|
||||
pollTask(existingTask.id, sourceId)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
// Missing existing task should fall through and create a new one.
|
||||
}
|
||||
|
||||
await createAndPoll(sourceId)
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
clearTimer()
|
||||
stopAudio()
|
||||
setTask(null)
|
||||
statusText.value = ''
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
onUnmounted(reset)
|
||||
|
||||
return {
|
||||
task,
|
||||
loading,
|
||||
playing,
|
||||
statusText,
|
||||
buttonText,
|
||||
playSource,
|
||||
reset
|
||||
}
|
||||
}
|
||||
|
||||
export default useTtsPlayer
|
||||
Reference in New Issue
Block a user