Files
sionrui/frontend/app/web-gold/src/views/content-style/Benchmark.vue
2026-01-17 19:33:59 +08:00

347 lines
9.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { ref, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { useRouter } from 'vue-router'
import { resolveId } from '@/utils/url'
import { exportBenchmarkDataToExcel } from '@/utils/excel'
import { usePromptStore } from '@/stores/prompt'
import GlobalLoading from '@/components/GlobalLoading.vue'
import TikhubService, { InterfaceType, MethodType } from '@/api/tikhub/index.js'
import { useBenchmarkData } from './composables/useBenchmarkData'
import { useBenchmarkAnalysis } from './composables/useBenchmarkAnalysis'
import { formatTime } from './utils/benchmarkUtils'
import BenchmarkForm from './components/BenchmarkForm.vue'
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,
saveTableDataToSession,
loadTableDataFromSession,
processApiResponse,
clearData,
} = useBenchmarkData()
// ==================== 分析功能 ====================
const {
loading,
batchAnalyzeLoading,
globalLoading,
globalLoadingText,
batchAnalyze,
getVoiceText,
} = useBenchmarkAnalysis(data, saveTableDataToSession)
// ==================== 表单状态 ====================
const form = ref({
platform: '抖音',
url: '',
count: 20,
sort_type: 0,
})
// ==================== 弹窗状态 ====================
const modalVisible = ref(false)
const batchPromptMergedText = ref('')
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'],
pathPatterns: ['/user/:id'],
})
if (!sec_user_id) {
return
}
loading.value = true
const req = await TikhubService.postTikHup({
type: InterfaceType.DOUYIN_WEB_USER_POST_VIDEOS,
methodType: MethodType.GET,
urlParams: {
sec_user_id,
max_cursor: 0,
type: 'tik-app',
sort_type: form.value.sort_type,
count: form.value.count || 20,
},
})
try {
const resp = await req
processApiResponse(resp, form.value.platform)
await saveTableDataToSession()
message.success('分析完成')
} catch (err) {
console.error(err)
message.error('请求失败,请稍后重试')
} finally {
loading.value = false
}
}
/**
* 导出数据到 Excel
*/
async function handleExportToExcel() {
if (!data.value || data.value.length === 0) {
message.warning('暂无数据可导出')
return
}
if (selectedRowKeys.value.length === 0) {
message.warning('请先选择要导出的行')
return
}
if (selectedRowKeys.value.length > 20) {
message.warning('最多只能导出20条数据请重新选择')
return
}
const selectedRows = data.value.filter(item => selectedRowKeys.value.includes(item.id))
const rowsNeedTranscription = selectedRows.filter(row => !row.transcriptions)
// 导出时只获取语音转写,不进行 AI 对话分析
if (rowsNeedTranscription.length > 0) {
globalLoading.value = true
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) {
const index = data.value.findIndex(item => item.id === row.id)
if (index !== -1) {
data.value[index].transcriptions = transcription.value
}
}
}
globalLoadingText.value = '正在导出...'
} catch (error) {
console.error('获取语音转写失败:', error)
message.warning('部分数据语音转写失败,将导出已有数据')
}
} else {
globalLoading.value = true
globalLoadingText.value = '正在导出数据...'
}
try {
const finalSelectedRows = data.value.filter(item => selectedRowKeys.value.includes(item.id))
const result = exportBenchmarkDataToExcel(finalSelectedRows, {
platform: form.value.platform,
formatTime
})
if (result.success) {
message.success(result.message)
} else {
message.error(result.message)
}
} finally {
globalLoading.value = false
globalLoadingText.value = ''
}
}
/**
* 批量分析处理
*/
async function handleBatchAnalyze() {
try {
await batchAnalyze(selectedRowKeys, async (mergedText, textCount) => {
batchPromptMergedText.value = mergedText
batchPromptTextCount.value = textCount
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()) {
message.warning('没有提示词可复制')
return
}
navigator.clipboard.writeText(prompt).then(() => {
message.success('提示词已复制到剪贴板')
}).catch(() => {
message.error('复制失败')
})
}
function handleUseBatchPrompt(prompt) {
if (!prompt || !prompt.trim()) {
message.warning('暂无批量生成的提示词')
return
}
promptStore.setPrompt(prompt, { batch: true })
router.push('/content-style/copywriting')
}
// ==================== 保存提示词到服务器 ====================
function handleOpenSavePromptModal(batchPrompt = null) {
// 批量提示词:使用传入的 batchPromptAI 生成的内容),而不是原始的 mergedText
const promptToSave = batchPrompt || batchPromptMergedText.value
if (!promptToSave || !promptToSave.trim()) {
message.warning('没有提示词可保存')
return
}
savePromptContent.value = promptToSave
savePromptModalVisible.value = true
}
// ==================== 生命周期 ====================
onMounted(async () => {
await loadTableDataFromSession()
})
defineOptions({ name: 'ContentStyleBenchmark' })
</script>
<template>
<div class="page">
<a-spin :spinning="batchAnalyzeLoading" tip="批量分析中,请稍候..." wrapperClassName="batch-analyze-spin-wrapper">
<div class="stack">
<!-- 表单区域 -->
<BenchmarkForm
v-model="form"
:loading="loading"
@analyze="handleAnalyzeUser"
@reset="handleResetForm"
/>
<!-- 表格区域 -->
<BenchmarkTable
:data="data"
v-model:selectedRowKeys="selectedRowKeys"
:loading="loading"
@export="handleExportToExcel"
@batch-analyze="handleBatchAnalyze"
/>
<!-- 空态显示 -->
<section class="card results-card empty-state" v-if="data.length === 0 && !loading">
<a-empty description="暂无数据,请点击开始分析">
<template #image>
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="20" y="30" width="80" height="60" rx="4" stroke="currentColor" stroke-width="2" fill="none" opacity="0.3"/>
<circle cx="40" cy="50" r="8" fill="currentColor" opacity="0.4"/>
<rect x="54" y="47" width="40" height="6" rx="3" fill="currentColor" opacity="0.4"/>
<rect x="54" y="60" width="32" height="6" rx="3" fill="currentColor" opacity="0.4"/>
<line x1="32" y1="75" x2="88" y2="75" stroke="currentColor" stroke-width="2" opacity="0.3"/>
<line x1="32" y1="82" x2="88" y2="82" stroke="currentColor" stroke-width="2" opacity="0.3"/>
<line x1="32" y1="89" x2="72" y2="89" stroke="currentColor" stroke-width="2" opacity="0.3"/>
</svg>
</template>
</a-empty>
</section>
</div>
</a-spin>
<!-- 批量分析结果弹窗 -->
<BatchAnalyzeModal
v-model:visible="modalVisible"
:merged-text="batchPromptMergedText"
:text-count="batchPromptTextCount"
@copy="handleCopyBatchPrompt"
@save="(prompt) => handleOpenSavePromptModal(prompt)"
@use="handleUseBatchPrompt"
/>
<!-- 保存提示词弹窗 -->
<SavePromptModal
v-model:visible="savePromptModalVisible"
:prompt-content="savePromptContent"
/>
<!-- 全局 Loading 遮罩 -->
<GlobalLoading :visible="globalLoading" :text="globalLoadingText" />
</div>
</template>
<style scoped>
/* 页面垂直堆叠间距 */
.stack>*+* {
margin-top: 16px;
}
/* 稳定滚动条,避免内容高度变化导致页面左右抖动 */
.page {
scrollbar-gutter: stable both-edges;
}
/* 卡片样式(不依赖 tailwind */
.card {
background: var(--color-surface);
border-radius: 8px;
padding: 16px;
box-shadow: var(--shadow-inset-card);
border: 1px solid var(--color-border);
}
/* 结果区预留最小高度,切换视图时避免高度突变 */
.results-card {
min-height: 420px;
}
/* 空态卡片样式 */
.empty-state {
display: flex;
align-items: center;
justify-content: center;
min-height: 420px;
}
.empty-state :deep(.ant-empty) {
padding: 40px 0;
}
.empty-state :deep(.ant-empty-description) {
color: var(--color-text-secondary);
font-size: 14px;
}
/* 全屏 Spin 遮罩样式 */
:deep(.batch-analyze-spin-wrapper) {
width: 100%;
min-height: calc(100vh - 120px);
}
:deep(.batch-analyze-spin-wrapper .ant-spin-spinning) {
max-height: none;
}
</style>