Files
sionrui/frontend/app/web-gold/src/views/content-style/Benchmark.vue
2026-02-25 23:44:01 +08:00

381 lines
11 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 { copyToClipboard } from '@/utils/clipboard'
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,
appendData,
clearData,
} = useBenchmarkData()
const {
loading,
batchAnalyzeLoading,
globalLoading,
globalLoadingText,
batchAnalyze,
getVoiceText,
} = 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('')
const maxCursor = ref(0)
const hasMore = ref(false)
const loadingMore = ref(false)
async function handleAnalyzeUser() {
const sec_user_id = resolveId(form.value.url, {
queryKeys: ['user'],
pathPatterns: ['/user/:id'],
})
if (!sec_user_id) {
return
}
loading.value = true
maxCursor.value = 0
hasMore.value = false
await clearData()
try {
const resp = 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,
},
})
const result = processApiResponse(resp, form.value.platform)
maxCursor.value = result.maxCursor
hasMore.value = result.hasMore
await saveTableDataToSession()
message.success('分析完成')
} catch (err) {
console.error(err)
message.error('请求失败,请稍后重试')
} finally {
loading.value = false
}
}
async function handleExportToExcel() {
if (!data.value?.length) {
message.warning('暂无数据可导出')
return
}
if (!selectedRowKeys.value.length) {
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)
globalLoading.value = true
globalLoadingText.value = rowsNeedTranscription.length ? '正在分析中...' : '正在导出数据...'
try {
if (rowsNeedTranscription.length) {
try {
const transcriptions = await getVoiceText(rowsNeedTranscription)
rowsNeedTranscription.forEach(row => {
const transcription = transcriptions.find(item => item.audio_url === row.audio_url)
const index = data.value.findIndex(item => item.id === row.id)
if (transcription && index !== -1) {
data.value[index].transcriptions = transcription.value
}
})
globalLoadingText.value = '正在导出...'
} catch (error) {
console.error('获取语音转写失败:', error)
message.warning('部分数据语音转写失败,将导出已有数据')
}
}
const finalSelectedRows = data.value.filter(item => selectedRowKeys.value.includes(item.id))
const result = exportBenchmarkDataToExcel(finalSelectedRows, {
platform: form.value.platform,
formatTime
})
message[result.success ? 'success' : '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 }
maxCursor.value = 0
hasMore.value = false
await clearData()
}
async function handleLoadMore() {
const sec_user_id = resolveId(form.value.url, {
queryKeys: ['user'],
pathPatterns: ['/user/:id'],
})
if (!sec_user_id) {
return
}
loadingMore.value = true
try {
const resp = await TikhubService.postTikHup({
type: InterfaceType.DOUYIN_WEB_USER_POST_VIDEOS,
methodType: MethodType.GET,
urlParams: {
sec_user_id,
max_cursor: maxCursor.value,
type: 'tik-app',
sort_type: form.value.sort_type,
count: form.value.count || 20,
},
})
if (form.value.platform === '抖音') {
const { mapFromDouyin } = await import('./utils/benchmarkUtils')
const awemeList = resp?.data?.aweme_list || []
const newItems = mapFromDouyin(awemeList)
appendData(newItems)
maxCursor.value = resp?.data?.max_cursor || 0
hasMore.value = resp?.data?.has_more || !(awemeList.length < (resp?.data?.count || 20))
} else {
const { mapFromXhs } = await import('./utils/benchmarkUtils')
const notes = resp?.data?.notes || resp?.data?.data || []
const newItems = mapFromXhs(notes)
appendData(newItems)
maxCursor.value = resp?.data?.max_cursor || 0
hasMore.value = resp?.data?.has_more || false
}
await saveTableDataToSession()
message.success(`已加载 ${data.value.length} 条数据`)
} catch (err) {
console.error(err)
message.error('加载失败,请稍后重试')
} finally {
loadingMore.value = false
}
}
function validatePrompt(prompt, warningMsg = '没有提示词') {
if (!prompt?.trim()) {
message.warning(warningMsg)
return false
}
return true
}
async function handleCopyBatchPrompt(prompt) {
if (!validatePrompt(prompt, '没有提示词可复制')) return
const success = await copyToClipboard(prompt)
message[success ? 'success' : 'error'](success ? '提示词已复制到剪贴板' : '复制失败')
}
function handleUseBatchPrompt(prompt) {
if (!validatePrompt(prompt, '暂无批量生成的提示词')) return
promptStore.setPrompt(prompt, { batch: true })
router.push('/content-style/copywriting')
}
function handleOpenSavePromptModal(batchPrompt = null) {
const promptToSave = batchPrompt || batchPromptMergedText.value
if (!validatePrompt(promptToSave, '没有提示词可保存')) 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"
:loading-more="loadingMore"
:has-more="hasMore"
@export="handleExportToExcel"
@batch-analyze="handleBatchAnalyze"
@load-more="handleLoadMore"
/>
<section v-if="!data.length" 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">
<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 {
overflow-x: hidden;
}
.stack>*+* {
margin-top: var(--space-4);
}
/* 稳定滚动条,只在垂直方向预留空间 */
.page {
overflow-x: hidden;
max-width: 100%;
}
/* 卡片样式 */
.card {
background: var(--color-bg-card);
border-radius: var(--radius-md);
padding: var(--space-4);
box-shadow: var(--shadow-sm);
border: 1px solid var(--color-gray-200);
}
/* 结果区预留最小高度,切换视图时避免高度突变 */
.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-gray-600);
font-size: var(--font-size-base);
}
/* 全屏 Spin 遮罩样式 */
:deep(.batch-analyze-spin-wrapper) {
width: 100%;
min-height: calc(100vh - 120px);
overflow-x: hidden;
}
:deep(.batch-analyze-spin-wrapper .ant-spin-spinning) {
max-height: none;
}
</style>