Files
sionrui/frontend/app/web-gold/src/views/content-style/Benchmark.vue
2025-11-13 01:06:28 +08:00

372 lines
10 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,
expandedRowKeys,
saveTableDataToSession,
loadTableDataFromSession,
processApiResponse,
clearData,
} = useBenchmarkData()
// ==================== 分析功能 ====================
const {
loading,
batchAnalyzeLoading,
globalLoading,
globalLoadingText,
analyzeVideo,
batchAnalyze,
} = useBenchmarkAnalysis(data, expandedRowKeys, 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 > 10) {
message.warning('最多只能导出10条数据请重新选择')
return
}
const selectedRows = data.value.filter(item => selectedRowKeys.value.includes(item.id))
const unanalyzedRows = selectedRows.filter(row => !row.transcriptions && !row.prompt)
if (unanalyzedRows.length > 0) {
globalLoading.value = true
globalLoadingText.value = `正在分析 ${unanalyzedRows.length} 条数据...`
try {
for (let i = 0; i < unanalyzedRows.length; i++) {
const row = unanalyzedRows[i]
globalLoadingText.value = `正在分析第 ${i + 1}/${unanalyzedRows.length} 条数据...`
await analyzeVideo(row)
}
globalLoadingText.value = '分析完成,正在导出...'
} catch (error) {
console.error('分析失败:', error)
message.error('部分数据分析失败,将导出已分析的数据')
}
} 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 handleCopyPrompt(row) {
if (!row.prompt) {
message.warning('没有提示词可复制')
return
}
navigator.clipboard.writeText(row.prompt).then(() => {
message.success('提示词已复制到剪贴板')
}).catch(() => {
message.error('复制失败')
})
}
function handleCopyBatchPrompt(prompt) {
if (!prompt || !prompt.trim()) {
message.warning('没有提示词可复制')
return
}
navigator.clipboard.writeText(prompt).then(() => {
message.success('提示词已复制到剪贴板')
}).catch(() => {
message.error('复制失败')
})
}
// ==================== 创作相关函数 ====================
function handleCreateContent(row) {
promptStore.setPrompt(row.prompt, row)
router.push('/content-style/copywriting')
}
function handleUseBatchPrompt(prompt) {
if (!prompt || !prompt.trim()) {
message.warning('暂无批量生成的提示词')
return
}
promptStore.setPrompt(prompt, { batch: true })
router.push('/content-style/copywriting')
}
// ==================== 保存提示词到服务器 ====================
function handleOpenSavePromptModal(row, batchPrompt = null) {
if (row) {
// 单个视频的提示词
if (!row.prompt || !row.prompt.trim()) {
message.warning('没有提示词可保存')
return
}
savePromptContent.value = row.prompt
} else {
// 批量提示词:使用传入的 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"
v-model:expandedRowKeys="expandedRowKeys"
:loading="loading"
@analyze="analyzeVideo"
@export="handleExportToExcel"
@batch-analyze="handleBatchAnalyze"
@copy="handleCopyPrompt"
@save-to-server="handleOpenSavePromptModal"
@create-content="handleCreateContent"
/>
<!-- 空态显示 -->
<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(null, 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>