372 lines
10 KiB
Vue
372 lines
10 KiB
Vue
<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 {
|
||
// 批量提示词:使用传入的 batchPrompt(AI 生成的内容),而不是原始的 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>
|