Add discovery search history with auto-save, fix timeout causing search failure
- Save every search result to DB for later review - Add '搜索历史' tab with timeline view, load/delete records - Raise discovery search timeout from 30s to 120s (Bing Puppeteer needs ~40s) - Reduce search queries from 4 to 3 for faster response - New model: DiscoveryRecord (user_id, product, market, companies JSON) - API: POST/GET/DELETE /api/v1/discovery/records - Migration: discovery_records table
This commit is contained in:
@@ -71,9 +71,13 @@ export function generateMarketing(data) { return http.post('/marketing/generate'
|
||||
export function getKeywords(data) { return http.post('/marketing/keywords', data) }
|
||||
export function competitorAnalysis(data) { return http.post('/marketing/competitor-analysis', data) }
|
||||
|
||||
export function discoverySearch(data) { return http.post('/discovery/search', data) }
|
||||
export function discoveryAnalyze(data) { return http.post('/discovery/analyze', data) }
|
||||
export function discoveryOutreach(data) { return http.post('/discovery/outreach', data) }
|
||||
export function discoverySearch(data) { return http.post('/discovery/search', data, { timeout: 120000 }) }
|
||||
export function discoveryAnalyze(data) { return http.post('/discovery/analyze', data, { timeout: 60000 }) }
|
||||
export function discoveryOutreach(data) { return http.post('/discovery/outreach', data, { timeout: 60000 }) }
|
||||
export function saveDiscoveryRecord(data) { return http.post('/discovery/records', data) }
|
||||
export function listDiscoveryRecords(params) { return http.get('/discovery/records', { params }) }
|
||||
export function getDiscoveryRecord(id) { return http.get(`/discovery/records/${id}`) }
|
||||
export function deleteDiscoveryRecord(id) { return http.delete(`/discovery/records/${id}`) }
|
||||
|
||||
export function getFollowupStats() { return http.get('/followup/stats') }
|
||||
export function getFollowupPending() { return http.get('/followup/pending') }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-tabs v-model="tab">
|
||||
<el-tabs v-model="tab" @tab-change="onTabChange">
|
||||
<el-tab-pane label="客户挖掘" name="search">
|
||||
<el-card shadow="never">
|
||||
<el-form :model="form" label-width="100">
|
||||
@@ -58,6 +58,33 @@
|
||||
<el-empty v-if="!loading && !results.length && searched" description="未找到匹配客户" :image-size="60" />
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="搜索历史" name="history">
|
||||
<el-card shadow="never">
|
||||
<div v-if="historyLoading" style="text-align:center;padding:40px">
|
||||
<el-icon class="is-loading" :size="24"><Loading /></el-icon>
|
||||
</div>
|
||||
<div v-else-if="history.length">
|
||||
<el-timeline>
|
||||
<el-timeline-item v-for="h in history" :key="h.id" :timestamp="formatTime(h.created_at)" placement="top">
|
||||
<el-card shadow="hover" style="cursor:pointer" @click="loadHistory(h)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<div>
|
||||
<strong>{{ h.product }}</strong>
|
||||
<el-tag v-if="h.market" size="small" style="margin-left:8px">{{ h.market }}</el-tag>
|
||||
</div>
|
||||
<el-button size="small" type="danger" link @click.stop="deleteRecord(h.id)">删除</el-button>
|
||||
</div>
|
||||
<div style="color:#999;font-size:12px;margin-top:4px">{{ h.companies?.length || 0 }} 条结果</div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<div v-if="hasMore" style="text-align:center;margin-top:16px">
|
||||
<el-button :loading="historyLoadingMore" @click="loadMoreHistory">加载更多</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="暂无搜索历史" :image-size="60" />
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="开发信生成" name="outreach">
|
||||
<el-card shadow="never">
|
||||
<el-form :model="outForm" label-width="100">
|
||||
@@ -83,14 +110,18 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { discoverySearch, discoveryOutreach, discoveryAnalyze, createCustomer } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
discoverySearch, discoveryOutreach, discoveryAnalyze, createCustomer,
|
||||
saveDiscoveryRecord, listDiscoveryRecords, getDiscoveryRecord, deleteDiscoveryRecord,
|
||||
} from '@/api'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const tab = ref('search')
|
||||
const loading = ref(false)
|
||||
const searched = ref(false)
|
||||
const results = ref([])
|
||||
const provider = ref('')
|
||||
const lastSearch = ref({ product: '', market: '' })
|
||||
|
||||
function openUrl(url) { window.open(url, '_blank') }
|
||||
const form = ref({ product: '', market: '' })
|
||||
@@ -98,21 +129,95 @@ const outForm = ref({ company: '', product: '', channel: 'email' })
|
||||
const outLoading = ref(false)
|
||||
const outreachResult = ref('')
|
||||
|
||||
const history = ref([])
|
||||
const historyLoading = ref(false)
|
||||
const historyLoadingMore = ref(false)
|
||||
const historyPage = ref(1)
|
||||
const hasMore = ref(false)
|
||||
|
||||
function scoreType(s) { if (s >= 80) return 'success'; if (s >= 60) return 'warning'; return 'info' }
|
||||
|
||||
function formatTime(iso) {
|
||||
if (!iso) return ''
|
||||
const d = new Date(iso)
|
||||
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`
|
||||
}
|
||||
|
||||
async function fetchHistory(page = 1, append = false) {
|
||||
try {
|
||||
if (!append) historyLoading.value = true
|
||||
else historyLoadingMore.value = true
|
||||
const res = await listDiscoveryRecords({ page, size: 20 })
|
||||
const d = res.data || res
|
||||
const items = d.items || d || []
|
||||
if (append) history.value.push(...items)
|
||||
else history.value = items
|
||||
hasMore.value = items.length >= 20
|
||||
historyPage.value = page
|
||||
} catch { /* ignore */ }
|
||||
finally {
|
||||
historyLoading.value = false
|
||||
historyLoadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHistory(h) {
|
||||
try {
|
||||
const res = await getDiscoveryRecord(h.id)
|
||||
const d = res.data || res
|
||||
results.value = (d.companies || []).map(r => ({ ...r, _contactDetail: null, _analyzing: false }))
|
||||
provider.value = 'history'
|
||||
form.value.product = d.product
|
||||
form.value.market = d.market
|
||||
searched.value = true
|
||||
tab.value = 'search'
|
||||
} catch { ElMessage.error('加载失败') }
|
||||
}
|
||||
|
||||
async function deleteRecord(id) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除该搜索记录?', '提示')
|
||||
await deleteDiscoveryRecord(id)
|
||||
history.value = history.value.filter(h => h.id !== id)
|
||||
ElMessage.success('已删除')
|
||||
} catch { /* cancel */ }
|
||||
}
|
||||
|
||||
function loadMoreHistory() {
|
||||
fetchHistory(historyPage.value + 1, true)
|
||||
}
|
||||
|
||||
function onTabChange(name) {
|
||||
if (name === 'history') fetchHistory()
|
||||
}
|
||||
|
||||
async function search() {
|
||||
if (!form.value.product) { ElMessage.warning('请输入产品'); return }
|
||||
loading.value = true
|
||||
searched.value = true
|
||||
lastSearch.value = { product: form.value.product, market: form.value.market }
|
||||
try {
|
||||
const res = await discoverySearch({
|
||||
product_description: form.value.product,
|
||||
target_market: form.value.market || 'US',
|
||||
})
|
||||
const d = res.data || res
|
||||
results.value = (d.companies || d.items || d.results || d || []).map(r => ({ ...r, _contactDetail: null, _analyzing: false }))
|
||||
const companies = d.companies || d.items || d.results || d || []
|
||||
results.value = companies.map(r => ({ ...r, _contactDetail: null, _analyzing: false }))
|
||||
provider.value = d.provider || ''
|
||||
} catch { ElMessage.error('挖掘失败') }
|
||||
|
||||
saveDiscoveryRecord({
|
||||
product: form.value.product,
|
||||
market: form.value.market || 'US',
|
||||
companies: companies.map(r => ({
|
||||
name: r.name, description: r.description, country: r.country,
|
||||
match_score: r.match_score, contact: r.contact, source: r.source,
|
||||
})),
|
||||
}).catch(() => {})
|
||||
} catch (e) {
|
||||
const msg = e?.detail || e?.message || '挖掘失败(超时或服务错误)'
|
||||
ElMessage.error(msg)
|
||||
}
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user