feat: 优化

This commit is contained in:
2026-01-18 18:36:37 +08:00
parent 265ee3a453
commit f5bccf8da4
11 changed files with 1435 additions and 252 deletions

View File

@@ -15,21 +15,19 @@ import BenchmarkTable from './components/BenchmarkTable.vue'
import BatchAnalyzeModal from './components/BatchAnalyzeModal.vue'
import SavePromptModal from './components/SavePromptModal.vue'
// ==================== 初始化 ====================
const router = useRouter()
const promptStore = usePromptStore()
// ==================== 数据管理 ====================
const {
data,
selectedRowKeys,
expandedRowKeys,
saveTableDataToSession,
loadTableDataFromSession,
processApiResponse,
clearData,
} = useBenchmarkData()
// ==================== 分析功能 ====================
const {
loading,
batchAnalyzeLoading,
@@ -37,9 +35,8 @@ const {
globalLoadingText,
batchAnalyze,
getVoiceText,
} = useBenchmarkAnalysis(data, saveTableDataToSession)
} = useBenchmarkAnalysis(data, expandedRowKeys, saveTableDataToSession)
// ==================== 表单状态 ====================
const form = ref({
platform: '抖音',
url: '',
@@ -47,7 +44,6 @@ const form = ref({
sort_type: 0,
})
// ==================== 弹窗状态 ====================
const modalVisible = ref(false)
const batchPromptMergedText = ref('')
const batchPromptTextCount = ref(0)
@@ -55,10 +51,6 @@ const batchPromptTextCount = ref(0)
const savePromptModalVisible = ref(false)
const savePromptContent = ref('')
// ==================== API 调用函数 ====================
/**
* 分析用户主页,获取视频列表
*/
async function handleAnalyzeUser() {
const sec_user_id = resolveId(form.value.url, {
queryKeys: ['user'],
@@ -94,16 +86,13 @@ async function handleAnalyzeUser() {
}
}
/**
* 导出数据到 Excel
*/
async function handleExportToExcel() {
if (!data.value || data.value.length === 0) {
if (!data.value?.length) {
message.warning('暂无数据可导出')
return
}
if (selectedRowKeys.value.length === 0) {
if (!selectedRowKeys.value.length) {
message.warning('请先选择要导出的行')
return
}
@@ -116,15 +105,13 @@ async function handleExportToExcel() {
const selectedRows = data.value.filter(item => selectedRowKeys.value.includes(item.id))
const rowsNeedTranscription = selectedRows.filter(row => !row.transcriptions)
// 导出时只获取语音转写,不进行 AI 对话分析
if (rowsNeedTranscription.length > 0) {
if (rowsNeedTranscription.length) {
globalLoading.value = true
globalLoadingText.value = `正在分析中...`
globalLoadingText.value = '正在分析中...'
try {
const transcriptions = await getVoiceText(rowsNeedTranscription)
// 更新转写数据
for (const row of rowsNeedTranscription) {
const transcription = transcriptions.find(item => item.audio_url === row.audio_url)
if (transcription) {
@@ -163,9 +150,6 @@ async function handleExportToExcel() {
}
}
/**
* 批量分析处理
*/
async function handleBatchAnalyze() {
try {
await batchAnalyze(selectedRowKeys, async (mergedText, textCount) => {
@@ -174,22 +158,17 @@ async function handleBatchAnalyze() {
modalVisible.value = true
})
} finally {
// 批量分析完成后清空选中项(无论成功还是失败)
selectedRowKeys.value = []
}
}
/**
* 重置表单
*/
async function handleResetForm() {
form.value = { platform: '抖音', url: '', count: 20, sort_type: 0 }
await clearData()
}
// ==================== 批量提示词操作函数 ====================
function handleCopyBatchPrompt(prompt) {
if (!prompt || !prompt.trim()) {
if (!prompt?.trim()) {
message.warning('没有提示词可复制')
return
}
@@ -202,7 +181,7 @@ function handleCopyBatchPrompt(prompt) {
}
function handleUseBatchPrompt(prompt) {
if (!prompt || !prompt.trim()) {
if (!prompt?.trim()) {
message.warning('暂无批量生成的提示词')
return
}
@@ -211,11 +190,9 @@ function handleUseBatchPrompt(prompt) {
router.push('/content-style/copywriting')
}
// ==================== 保存提示词到服务器 ====================
function handleOpenSavePromptModal(batchPrompt = null) {
// 批量提示词:使用传入的 batchPromptAI 生成的内容),而不是原始的 mergedText
const promptToSave = batchPrompt || batchPromptMergedText.value
if (!promptToSave || !promptToSave.trim()) {
if (!promptToSave?.trim()) {
message.warning('没有提示词可保存')
return
}
@@ -223,7 +200,6 @@ function handleOpenSavePromptModal(batchPrompt = null) {
savePromptModalVisible.value = true
}
// ==================== 生命周期 ====================
onMounted(async () => {
await loadTableDataFromSession()
})
@@ -252,8 +228,7 @@ defineOptions({ name: 'ContentStyleBenchmark' })
@batch-analyze="handleBatchAnalyze"
/>
<!-- 空态显示 -->
<section class="card results-card empty-state" v-if="data.length === 0 && !loading">
<section v-if="!data.length && !loading" class="card results-card empty-state">
<a-empty description="暂无数据,请点击开始分析">
<template #image>
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">

View File

@@ -1,41 +1,44 @@
<script setup>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { EditOutlined, CopyOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import ChatMessageRenderer from '@/components/ChatMessageRenderer.vue'
import ChatMessageRendererV2 from '@/components/ChatMessageRendererV2.vue'
import { ChatMessageApi } from '@/api/chat'
import { streamChat } from '@/utils/streamChat'
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
mergedText: {
type: String,
default: '',
},
textCount: {
type: Number,
default: 0,
},
const props = withDefaults(defineProps<{
visible: boolean
mergedText: string
textCount: number
}>(), {
visible: false,
mergedText: '',
textCount: 0,
})
const emit = defineEmits(['update:visible', 'copy', 'save', 'use'])
const emit = defineEmits<{
'update:visible': [value: boolean]
'copy': [text: string]
'save': [text: string]
'use': [text: string]
}>()
const batchPrompt = ref('')
const batchPromptEditMode = ref(false)
const batchPromptGenerating = ref(false)
const hasGenerated = ref(false)
function resetModal() {
batchPrompt.value = ''
batchPromptEditMode.value = false
batchPromptGenerating.value = false
hasGenerated.value = false
}
watch(() => props.visible, (newVal) => {
if (newVal && props.mergedText && !hasGenerated.value) {
generateBatchPrompt()
} else if (!newVal) {
batchPrompt.value = ''
batchPromptEditMode.value = false
batchPromptGenerating.value = false
hasGenerated.value = false
resetModal()
}
})
@@ -47,47 +50,61 @@ watch(() => props.mergedText, (newVal) => {
async function generateBatchPrompt() {
if (!props.mergedText || hasGenerated.value) return
hasGenerated.value = true
try {
batchPromptGenerating.value = true
const createPayload = { roleId: 20 }
console.debug('createChatConversationMy payload(batch):', createPayload)
const conversationResp = await ChatMessageApi.createChatConversationMy(createPayload)
let conversationId = null
if (conversationResp?.data) {
conversationId = typeof conversationResp.data === 'object' ? conversationResp.data.id : conversationResp.data
}
if (!conversationId) {
throw new Error('创建对话失败:未获取到 conversationId')
}
const conversationId = await createConversation()
const aiContent = await streamChat({
conversationId,
content: props.mergedText,
onUpdate: (fullText) => {
onUpdate: (fullText: string) => {
batchPrompt.value = fullText
},
enableTypewriter: true,
typewriterSpeed: 10,
typewriterBatchSize: 2
typewriterBatchSize: 2,
onComplete: () => {},
onError: (error: Error) => {
console.error('流式聊天错误:', error)
},
enableContext: false,
enableWebSearch: false,
timeout: 180000,
attachmentUrls: []
})
if (aiContent && aiContent !== batchPrompt.value) {
batchPrompt.value = aiContent
}
message.success(`批量分析完成:已基于 ${props.textCount} 个视频的文案生成综合提示词`)
} catch (aiError) {
console.error('AI生成失败:', aiError)
} catch (error) {
console.error('AI生成失败:', error)
message.error('AI生成失败请稍后重试')
} finally {
batchPromptGenerating.value = false
hasGenerated.value = false
}
}
async function createConversation() {
const createPayload = { roleId: 20 }
const conversationResp = await ChatMessageApi.createChatConversationMy(createPayload)
const conversationId = conversationResp?.data
? (typeof conversationResp.data === 'object' ? conversationResp.data.id : conversationResp.data)
: null
if (!conversationId) {
throw new Error('创建对话失败:未获取到 conversationId')
}
return conversationId
}
function handleClose() {
emit('update:visible', false)
}
@@ -112,131 +129,86 @@ function handleUse() {
:width="800"
:maskClosable="false"
:keyboard="false"
@cancel="handleClose">
@cancel="handleClose"
>
<div class="batch-prompt-modal">
<!-- 内容显示模式 -->
<div v-if="!batchPromptEditMode" class="batch-prompt-display">
<ChatMessageRenderer
<ChatMessageRendererV2
:content="batchPrompt"
:is-streaming="batchPromptGenerating"
/>
</div>
<a-textarea
<!-- 编辑模式 -->
<a-textarea
v-else
v-model:value="batchPrompt"
:rows="15"
placeholder="内容将在这里显示..." />
v-model:value="batchPrompt"
:rows="15"
placeholder="内容将在这里显示..."
/>
</div>
<template #footer>
<a-space>
<a-button size="small" :title="batchPromptEditMode ? '取消编辑' : '编辑'" @click="batchPromptEditMode = !batchPromptEditMode">
<template #icon>
<EditOutlined />
</template>
</a-button>
<a-button size="small" title="复制" @click="handleCopy">
<template #icon>
<CopyOutlined />
</template>
</a-button>
<a-button size="small" title="保存提示词" @click="handleSave" :disabled="!batchPrompt.trim()">
保存提示词
</a-button>
<a-button @click="handleClose">取消</a-button>
<a-button
type="primary"
:disabled="batchPromptGenerating || !batchPrompt.trim()"
@click="handleUse">去创作</a-button>
</a-space>
<div class="footer-actions">
<div class="left-actions">
<a-button type="text" @click="batchPromptEditMode = !batchPromptEditMode">
{{ batchPromptEditMode ? '取消编辑' : '编辑' }}
</a-button>
<a-button type="text" @click="handleCopy">复制</a-button>
<a-button
type="text"
@click="handleSave"
:disabled="!batchPrompt.trim()"
>
保存提示词
</a-button>
</div>
<div class="right-actions">
<a-button @click="handleClose">取消</a-button>
<a-button
type="primary"
:disabled="batchPromptGenerating || !batchPrompt.trim()"
@click="handleUse"
>
去创作
</a-button>
</div>
</div>
</template>
</a-modal>
</template>
<style scoped>
<style scoped lang="less">
.batch-prompt-modal {
min-height: 200px;
.batch-prompt-display {
min-height: 300px;
max-height: 500px;
overflow-y: auto;
padding: 24px;
border: 1px solid var(--color-border);
border-radius: 8px;
background: var(--color-surface);
}
}
.batch-prompt-display {
min-height: 300px;
max-height: 500px;
overflow-y: auto;
padding: 12px;
background: #0d0d0d;
border: 1px solid var(--color-border);
border-radius: 6px;
line-height: 1.6;
}
.footer-actions {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
.batch-prompt-display :deep(h1) {
font-size: 18px;
font-weight: 600;
margin: 12px 0;
color: var(--color-text);
}
.left-actions {
display: flex;
gap: 8px;
}
.batch-prompt-display :deep(h2) {
font-size: 16px;
font-weight: 600;
margin: 16px 0 8px 0;
color: var(--color-text);
}
.batch-prompt-display :deep(h3) {
font-size: 14px;
font-weight: 600;
margin: 12px 0 6px 0;
color: var(--color-text-secondary);
}
.batch-prompt-display :deep(p) {
margin: 8px 0;
color: var(--color-text-secondary);
}
.batch-prompt-display :deep(ul),
.batch-prompt-display :deep(ol) {
margin: 8px 0;
padding-left: 20px;
}
.batch-prompt-display :deep(li) {
margin: 4px 0;
color: var(--color-text-secondary);
}
.batch-prompt-display :deep(strong) {
font-weight: 600;
color: var(--color-text);
}
.batch-prompt-display :deep(code) {
background: #1a1a1a;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 12px;
color: #e11d48;
}
.batch-prompt-display :deep(pre) {
background: #1a1a1a;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin: 8px 0;
}
.batch-prompt-display :deep(pre code) {
background: transparent;
padding: 0;
}
.batch-prompt-display :deep(blockquote) {
border-left: 3px solid var(--color-primary);
padding-left: 12px;
margin: 8px 0;
color: var(--color-text-secondary);
.right-actions {
display: flex;
gap: 8px;
}
}
</style>

View File

@@ -1,6 +1,6 @@
<script setup>
import { CopyOutlined, SaveOutlined } from '@ant-design/icons-vue'
import ChatMessageRenderer from '@/components/ChatMessageRenderer.vue'
import ChatMessageRendererV2 from '@/components/ChatMessageRendererV2.vue'
const props = defineProps({
record: {
@@ -26,7 +26,6 @@ function handleCreateContent() {
<template>
<div class="expanded-content">
<!-- 未分析的行显示提示 -->
<div v-if="!record.transcriptions && !record.prompt" class="no-analysis-tip">
<a-empty description="该视频尚未分析">
<template #image>
@@ -42,10 +41,8 @@ function handleCreateContent() {
</a-button>
</a-empty>
</div>
<!-- 已分析的行显示内容 -->
<div v-else class="two-col">
<!-- 左侧原配音内容 -->
<section class="col left-col">
<div class="sub-title">原配音</div>
<div class="transcript-box" v-if="record.transcriptions">
@@ -54,44 +51,43 @@ function handleCreateContent() {
<div v-else class="no-transcript">暂无转写文本请先点击"分析"获取</div>
</section>
<!-- 右侧提示词 -->
<section class="col right-col">
<div class="sub-title">提示词</div>
<div class="prompt-display-wrapper">
<ChatMessageRenderer
<ChatMessageRendererV2
:content="record.prompt || ''"
:is-streaming="record._analyzing || false"
/>
<div v-if="!record.prompt" class="no-prompt">暂无提示词</div>
</div>
<div class="right-actions">
<a-space>
<a-button
size="small"
type="text"
<a-button
size="small"
type="text"
class="copy-btn"
:title="'复制'"
title="复制"
@click="handleCopy">
<template #icon>
<CopyOutlined />
</template>
</a-button>
<a-button
<a-button
v-if="record.prompt"
size="small"
type="text"
size="small"
type="text"
class="save-server-btn"
:title="'保存'"
title="保存"
@click="handleSaveToServer">
<template #icon>
<SaveOutlined />
</template>
保存
</a-button>
<a-button
type="dashed"
<a-button
type="dashed"
:disabled="!record.prompt || record._analyzing"
@click="handleCreateContent">基于提示词去创作</a-button>
</a-space>
@@ -177,7 +173,6 @@ function handleCreateContent() {
opacity: 0.8;
}
.no-analysis-tip {
padding: var(--space-8) var(--space-5);
text-align: center;

View File

@@ -12,43 +12,35 @@ export function useBenchmarkAnalysis(data, expandedRowKeys, saveTableDataToSessi
const globalLoadingText = ref('')
const { getVoiceText } = useVoiceText()
/**
* 分析单个视频,获取提示词
*/
async function analyzeVideo(row) {
try {
if (row._analyzing) return
row._analyzing = true
// 1) 获取音频转写
message.info('正在获取音频转写...')
const transcriptions = await getVoiceText([row])
row.transcriptions = transcriptions.find(item => item.audio_url === row.audio_url)?.value
// 2) 检查是否有语音文案
if (!row.transcriptions || !row.transcriptions.trim()) {
if (!row.transcriptions?.trim()) {
message.warning('未提取到语音内容,请检查音频文件或稍后重试')
row._analyzing = false
return false
}
// 3) 创建对话
message.info('正在创建对话...')
const createPayload = { roleId: 20, role_id: 20 }
console.debug('createChatConversationMy payload:', createPayload)
const conversationResp = await ChatMessageApi.createChatConversationMy(createPayload)
let conversationId = null
if (conversationResp?.data) {
conversationId = typeof conversationResp.data === 'object' ? conversationResp.data.id : conversationResp.data
}
const conversationId = conversationResp?.data
? (typeof conversationResp.data === 'object' ? conversationResp.data.id : conversationResp.data)
: null
if (!conversationId) {
throw new Error('创建对话失败:未获取到 conversationId')
}
// 4) 基于转写构建提示,流式生成并实时写入 UI
message.info('正在生成提示词...')
const content = buildPromptFromTranscription(row.transcriptions)
const index = data.value.findIndex(item => item.id === row.id)
@@ -63,19 +55,16 @@ export function useBenchmarkAnalysis(data, expandedRowKeys, saveTableDataToSessi
typewriterBatchSize: 2
})
// 5) 兜底处理
const finalPrompt = aiContent || row.transcriptions || ''
if (index !== -1) data.value[index].prompt = finalPrompt
// 6) 分析完成后自动展开该行
const rowId = String(row.id) // 确保类型一致
const rowId = String(row.id)
if (!expandedRowKeys.value.includes(rowId)) {
expandedRowKeys.value.push(rowId)
}
// 7) 保存数据到 session
await saveTableDataToSession()
message.success('分析完成')
return true
} catch (error) {
@@ -87,11 +76,8 @@ export function useBenchmarkAnalysis(data, expandedRowKeys, saveTableDataToSessi
}
}
/**
* 批量分析选中的视频
*/
async function batchAnalyze(selectedRowKeys, onBatchComplete) {
if (selectedRowKeys.value.length === 0) {
if (!selectedRowKeys.value.length) {
message.warning('请先选择要分析的视频')
return
}
@@ -101,26 +87,23 @@ export function useBenchmarkAnalysis(data, expandedRowKeys, saveTableDataToSessi
globalLoadingText.value = `正在批量分析 ${selectedRowKeys.value.length} 个视频...`
try {
// 1. 获取所有选中视频的语音转写
globalLoadingText.value = '正在获取中...'
const selectedRows = data.value.filter(item => selectedRowKeys.value.includes(item.id))
const transcriptions = await getVoiceText(selectedRows)
// 2. 收集所有转写内容
const allTexts = []
for (const id of selectedRowKeys.value) {
const row = data.value.find(item => item.id === id)
if (row && row.audio_url) {
if (row?.audio_url) {
const transcription = transcriptions.find(item => item.audio_url === row.audio_url)
if (transcription && transcription.value && transcription.value.trim()) {
if (transcription?.value?.trim()) {
allTexts.push({ id: row.id, url: row.audio_url, text: transcription.value })
row.transcriptions = transcription.value
}
}
}
// 3. 检查是否有可用的语音内容
if (allTexts.length === 0) {
if (!allTexts.length) {
message.warning('未提取到任何语音内容,请检查音频文件或稍后重试')
batchAnalyzeLoading.value = false
globalLoading.value = false
@@ -130,8 +113,7 @@ export function useBenchmarkAnalysis(data, expandedRowKeys, saveTableDataToSessi
await saveTableDataToSession()
const mergedText = allTexts.map(item => item.text).join('\n\n---\n\n')
// 4. 通知父组件打开弹窗并开始生成
if (onBatchComplete) {
await onBatchComplete(mergedText, allTexts.length)
}

View File

@@ -9,12 +9,8 @@ export function useBenchmarkData() {
const selectedRowKeys = ref([])
const expandedRowKeys = ref([])
/**
* 保存表格数据到 session
*/
async function saveTableDataToSession() {
try {
// 过滤掉不需要持久化的临时字段(如 _analyzing
const persistData = (data.value || []).map((item) => {
const rest = { ...item }
delete rest._analyzing
@@ -26,14 +22,10 @@ export function useBenchmarkData() {
}
}
/**
* 从 session 加载表格数据
*/
async function loadTableDataFromSession() {
try {
const savedData = await storage.getJSON(TABLE_DATA_STORAGE_KEY)
if (savedData && Array.isArray(savedData) && savedData.length > 0) {
// 强制恢复临时字段的初始状态
if (savedData?.length) {
data.value = savedData.map((item) => ({ ...item, _analyzing: false }))
console.log('从session加载了表格数据:', savedData.length, '条')
}
@@ -42,9 +34,6 @@ export function useBenchmarkData() {
}
}
/**
* 处理 API 响应数据
*/
function processApiResponse(resp, platform) {
if (platform === '抖音') {
const awemeList = resp?.data?.aweme_list || []
@@ -57,9 +46,6 @@ export function useBenchmarkData() {
}
}
/**
* 清空数据
*/
async function clearData() {
data.value = []
selectedRowKeys.value = []