Files
sionrui/frontend/app/web-gold/src/views/content-style/Benchmark.vue

372 lines
10 KiB
Vue
Raw Normal View History

2025-11-10 00:59:40 +08:00
<script setup>
2025-11-13 01:06:28 +08:00
import { ref, onMounted } from 'vue'
2025-11-10 00:59:40 +08:00
import { message } from 'ant-design-vue'
2025-11-13 01:06:28 +08:00
import { useRouter } from 'vue-router'
2025-11-10 00:59:40 +08:00
import { resolveId } from '@/utils/url'
2025-11-10 23:53:05 +08:00
import { exportBenchmarkDataToExcel } from '@/utils/excel'
2025-11-10 00:59:40 +08:00
import { usePromptStore } from '@/stores/prompt'
2025-11-10 23:53:05 +08:00
import GlobalLoading from '@/components/GlobalLoading.vue'
2025-11-13 01:06:28 +08:00
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'
2025-11-10 00:59:40 +08:00
// ==================== 初始化 ====================
const router = useRouter()
const promptStore = usePromptStore()
2025-11-13 01:06:28 +08:00
// ==================== 数据管理 ====================
const {
data,
selectedRowKeys,
expandedRowKeys,
saveTableDataToSession,
loadTableDataFromSession,
processApiResponse,
clearData,
} = useBenchmarkData()
// ==================== 分析功能 ====================
const {
loading,
batchAnalyzeLoading,
globalLoading,
globalLoadingText,
analyzeVideo,
batchAnalyze,
} = useBenchmarkAnalysis(data, expandedRowKeys, saveTableDataToSession)
// ==================== 表单状态 ====================
2025-11-10 00:59:40 +08:00
const form = ref({
platform: '抖音',
url: '',
count: 20,
sort_type: 0,
})
2025-11-13 01:06:28 +08:00
// ==================== 弹窗状态 ====================
const modalVisible = ref(false)
const batchPromptMergedText = ref('')
const batchPromptTextCount = ref(0)
2025-11-10 00:59:40 +08:00
2025-11-13 01:06:28 +08:00
const savePromptModalVisible = ref(false)
const savePromptContent = ref('')
2025-11-10 00:59:40 +08:00
2025-11-13 01:06:28 +08:00
// ==================== API 调用函数 ====================
2025-11-10 00:59:40 +08:00
/**
2025-11-13 01:06:28 +08:00
* 分析用户主页获取视频列表
2025-11-10 00:59:40 +08:00
*/
2025-11-13 01:06:28 +08:00
async function handleAnalyzeUser() {
const sec_user_id = resolveId(form.value.url, {
queryKeys: ['user'],
pathPatterns: ['/user/:id'],
})
if (!sec_user_id) {
return
}
2025-11-10 00:59:40 +08:00
2025-11-13 01:06:28 +08:00
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,
},
})
2025-11-10 00:59:40 +08:00
2025-11-13 01:06:28 +08:00
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
}
2025-11-10 00:59:40 +08:00
}
2025-11-10 23:53:05 +08:00
/**
2025-11-13 01:06:28 +08:00
* 导出数据到 Excel
2025-11-10 23:53:05 +08:00
*/
2025-11-13 01:06:28 +08:00
async function handleExportToExcel() {
2025-11-10 23:53:05 +08:00
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 = ''
}
}
2025-11-10 00:59:40 +08:00
/**
2025-11-13 01:06:28 +08:00
* 批量分析处理
2025-11-10 00:59:40 +08:00
*/
2025-11-13 01:06:28 +08:00
async function handleBatchAnalyze() {
2025-11-10 00:59:40 +08:00
try {
2025-11-13 01:06:28 +08:00
await batchAnalyze(selectedRowKeys, async (mergedText, textCount) => {
batchPromptMergedText.value = mergedText
batchPromptTextCount.value = textCount
modalVisible.value = true
2025-11-10 00:59:40 +08:00
})
} finally {
2025-11-13 01:06:28 +08:00
// 批量分析完成后清空选中项(无论成功还是失败)
selectedRowKeys.value = []
2025-11-10 00:59:40 +08:00
}
}
/**
2025-11-13 01:06:28 +08:00
* 重置表单
2025-11-10 00:59:40 +08:00
*/
2025-11-13 01:06:28 +08:00
async function handleResetForm() {
2025-11-10 00:59:40 +08:00
form.value = { platform: '抖音', url: '', count: 20, sort_type: 0 }
2025-11-13 01:06:28 +08:00
await clearData()
2025-11-10 00:59:40 +08:00
}
// ==================== 提示词操作函数 ====================
2025-11-13 01:06:28 +08:00
function handleCopyPrompt(row) {
2025-11-10 00:59:40 +08:00
if (!row.prompt) {
message.warning('没有提示词可复制')
return
}
navigator.clipboard.writeText(row.prompt).then(() => {
message.success('提示词已复制到剪贴板')
}).catch(() => {
message.error('复制失败')
})
}
2025-11-13 01:06:28 +08:00
function handleCopyBatchPrompt(prompt) {
if (!prompt || !prompt.trim()) {
2025-11-10 00:59:40 +08:00
message.warning('没有提示词可复制')
return
}
2025-11-13 01:06:28 +08:00
navigator.clipboard.writeText(prompt).then(() => {
2025-11-10 00:59:40 +08:00
message.success('提示词已复制到剪贴板')
}).catch(() => {
message.error('复制失败')
})
}
2025-11-13 01:06:28 +08:00
// ==================== 创作相关函数 ====================
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
}
2025-11-10 23:53:05 +08:00
2025-11-10 00:59:40 +08:00
// ==================== 生命周期 ====================
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">
2025-11-13 01:06:28 +08:00
<!-- 表单区域 -->
<BenchmarkForm
v-model="form"
:loading="loading"
@analyze="handleAnalyzeUser"
@reset="handleResetForm"
/>
<!-- 表格区域 -->
<BenchmarkTable
:data="data"
v-model:selectedRowKeys="selectedRowKeys"
2025-11-10 23:53:05 +08:00
v-model:expandedRowKeys="expandedRowKeys"
:loading="loading"
2025-11-13 01:06:28 +08:00
@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>
2025-11-10 00:59:40 +08:00
</template>
2025-11-13 01:06:28 +08:00
</a-empty>
</section>
2025-11-10 00:59:40 +08:00
</div>
</a-spin>
2025-11-13 01:06:28 +08:00
<!-- 批量分析结果弹窗 -->
<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"
/>
2025-11-10 23:53:05 +08:00
<!-- 全局 Loading 遮罩 -->
<GlobalLoading :visible="globalLoading" :text="globalLoadingText" />
2025-11-10 00:59:40 +08:00
</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>