381 lines
11 KiB
Vue
381 lines
11 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 { 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>
|