feat: add AI Digital Employee agent orchestrator with pipeline tracking
- New AgentPipeline model with JSONB pipeline_data for stages/leads/summary
- AgentOrchestrator service chains DiscoveryService search→analyze→outreach→auto-save
- 3 new API endpoints: POST /agent/start, GET /agent/pipelines, GET /agent/{id}
- Full Agent dashboard Vue component with stats, pipeline grid, leads table, outreach preview
- Sidebar redesigned with AI Agent as primary entry point
- Updated PROGRESS.md, AGENTS.md, DATABASE_SCHEMA.md with latest state
This commit is contained in:
Generated
-39
@@ -707,9 +707,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -724,9 +721,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -741,9 +735,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -758,9 +749,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -775,9 +763,6 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -792,9 +777,6 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -809,9 +791,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -826,9 +805,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -843,9 +819,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -860,9 +833,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -877,9 +847,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -894,9 +861,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -911,9 +875,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
@@ -131,4 +131,8 @@ export function subscribeCreditPlan(planId, payType = 'alipay') {
|
||||
}
|
||||
export function cancelCreditSubscription() { return http.post('/credits/cancel-subscription') }
|
||||
|
||||
export function startAgentPipeline(data) { return http.post('/agent/start', data, { timeout: 300000 }) }
|
||||
export function listAgentPipelines(params) { return http.get('/agent/pipelines', { params }) }
|
||||
export function getAgentPipeline(id) { return http.get(`/agent/${id}`) }
|
||||
|
||||
export default http
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
:collapse-transition="false"
|
||||
@select="showMobileMenu = false"
|
||||
>
|
||||
<el-menu-item index="/agent"><el-icon><MagicStick /></el-icon><span>{{ $t('nav.agent') || 'AI数字员工' }}</span></el-menu-item>
|
||||
<el-menu-item index="/discovery"><el-icon><Search /></el-icon><span>{{ $t('nav.discovery') }}</span></el-menu-item>
|
||||
<el-menu-item index="/workspace"><el-icon><Odometer /></el-icon><span>{{ $t('nav.workspace') }}</span></el-menu-item>
|
||||
<el-menu-item index="/customers"><el-icon><User /></el-icon><span>{{ $t('nav.customers') }}</span></el-menu-item>
|
||||
@@ -137,13 +138,6 @@ const beianInfo = computed(() => {
|
||||
return { icp: '京ICP备2026007249号-1', gongan: '京公网安备11011502039545号', gonganLink: 'https://beian.mps.gov.cn/#/query/webSearch?code=11011502039545', showGongan: true }
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await getUnreadCount()
|
||||
unread.value = res.count || res || 0
|
||||
} catch { /* ignore */ }
|
||||
})
|
||||
|
||||
function handleLogout() {
|
||||
auth.logout()
|
||||
router.push('/')
|
||||
|
||||
@@ -3,6 +3,14 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
const routes = [
|
||||
{ path: '/login', redirect: '/' },
|
||||
{ path: '/', name: 'Landing', component: () => import('@/views/WorkspaceLanding.vue') },
|
||||
{
|
||||
path: '/agent',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Agent', component: () => import('@/views/Agent.vue'), meta: { title: 'AI数字员工' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/workspace',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
|
||||
@@ -0,0 +1,531 @@
|
||||
<template>
|
||||
<div class="agent-dashboard">
|
||||
<!-- Header -->
|
||||
<div class="agent-header">
|
||||
<div class="agent-header-left">
|
||||
<h2>{{ $t('agent.title') || 'AI 数字员工' }}</h2>
|
||||
<p class="agent-subtitle">{{ $t('agent.subtitle') || '智能挖掘 · 分析 · 触达 · 跟进,一站式自动完成' }}</p>
|
||||
</div>
|
||||
<el-button type="primary" size="large" @click="showStartDialog = true" :icon="Plus">
|
||||
{{ $t('agent.newTask') || '新建任务' }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<el-row :gutter="16" class="agent-stats">
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-card shadow="never">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ stats.total }}</div>
|
||||
<div class="stat-label">{{ $t('agent.totalTasks') || '总任务' }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-card shadow="never">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" style="color:#67c23a">{{ stats.completed }}</div>
|
||||
<div class="stat-label">{{ $t('agent.completed') || '已完成' }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-card shadow="never">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" style="color:#e6a23c">{{ stats.total_leads }}</div>
|
||||
<div class="stat-label">{{ $t('agent.totalLeads') || '累计线索' }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-card shadow="never">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" style="color:#409eff">{{ stats.saved_customers }}</div>
|
||||
<div class="stat-label">{{ $t('agent.savedCustomers') || '已保存客户' }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- Pipeline List -->
|
||||
<div class="agent-section">
|
||||
<div class="section-header">
|
||||
<h3>{{ $t('agent.taskHistory') || '任务历史' }}</h3>
|
||||
<el-radio-group v-model="statusFilter" size="small">
|
||||
<el-radio-button value="">{{ $t('agent.all') || '全部' }}</el-radio-button>
|
||||
<el-radio-button value="running">{{ $t('agent.running') || '进行中' }}</el-radio-button>
|
||||
<el-radio-button value="completed">{{ $t('agent.done') || '已完成' }}</el-radio-button>
|
||||
<el-radio-button value="failed">{{ $t('agent.failed') || '失败' }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" style="text-align:center;padding:60px">
|
||||
<el-icon class="is-loading" :size="32"><Loading /></el-icon>
|
||||
</div>
|
||||
|
||||
<template v-else-if="filteredPipelines.length">
|
||||
<div class="pipeline-grid">
|
||||
<el-card
|
||||
v-for="p in filteredPipelines"
|
||||
:key="p.id"
|
||||
shadow="hover"
|
||||
:class="['pipeline-card', { active: selectedId === p.id }]"
|
||||
@click="selectPipeline(p)"
|
||||
>
|
||||
<div class="pipeline-card-header">
|
||||
<el-tag :type="statusTag(p.status)" size="small">{{ statusLabel(p.status) }}</el-tag>
|
||||
<el-tag v-if="p.progress === 100" type="success" size="small" effect="dark">{{ p.progress }}%</el-tag>
|
||||
<el-tag v-else type="warning" size="small" effect="plain">{{ p.progress || 0 }}%</el-tag>
|
||||
</div>
|
||||
<div class="pipeline-card-body">
|
||||
<h4>{{ p.product_name }}</h4>
|
||||
<p class="pipeline-market">{{ p.target_market }}</p>
|
||||
<p v-if="p.pipeline_data?.summary" class="pipeline-summary">
|
||||
发现 <strong>{{ p.pipeline_data.summary.total_leads || 0 }}</strong> 个线索,
|
||||
高匹配 <strong>{{ p.pipeline_data.summary.high_match || 0 }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="pipeline-card-footer">
|
||||
<span class="pipeline-time">{{ formatTime(p.created_at) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Progress bar for running -->
|
||||
<el-progress
|
||||
v-if="p.status === 'running'"
|
||||
:percentage="p.progress || 0"
|
||||
:stroke-width="3"
|
||||
style="margin-top:8px"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<div v-if="totalPages > 1" class="pagination-wrap">
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:total="totalPipelines"
|
||||
:page-size="pageSize"
|
||||
v-model:current-page="currentPage"
|
||||
@current-change="loadPipelines"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-empty v-else :description="$t('agent.noTasks') || '暂无任务,点击右上角新建'" :image-size="80" />
|
||||
</div>
|
||||
|
||||
<!-- Pipeline Detail -->
|
||||
<el-card v-if="selectedPipeline" shadow="never" class="pipeline-detail">
|
||||
<template #header>
|
||||
<div class="detail-header">
|
||||
<div>
|
||||
<strong>{{ selectedPipeline.product_name }}</strong>
|
||||
<el-tag :type="statusTag(selectedPipeline.status)" size="small" style="margin-left:8px">
|
||||
{{ statusLabel(selectedPipeline.status) }}
|
||||
</el-tag>
|
||||
<span style="color:#999;font-size:12px;margin-left:12px">{{ selectedPipeline.target_market }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<el-button size="small" @click="selectedPipeline = null">{{ $t('common.close') || '关闭' }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Stage Progress -->
|
||||
<div class="stage-steps">
|
||||
<div
|
||||
v-for="(st, stKey) in selectedPipeline.pipeline_data?.stages || {}"
|
||||
:key="stKey"
|
||||
:class="['stage-step', st.status]"
|
||||
>
|
||||
<div class="stage-icon">
|
||||
<el-icon v-if="st.status === 'completed'" color="#67c23a"><CircleCheck /></el-icon>
|
||||
<el-icon v-else-if="st.status === 'running'" class="is-loading" color="#409eff"><Loading /></el-icon>
|
||||
<el-icon v-else color="#999"><CircleClose /></el-icon>
|
||||
</div>
|
||||
<div class="stage-content">
|
||||
<div class="stage-name">{{ stageLabel(stKey) }}</div>
|
||||
<div class="stage-msg">{{ st.message || '' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leads Table -->
|
||||
<div v-if="leads.length" class="leads-section">
|
||||
<h4 style="margin:16px 0 12px">{{ $t('agent.leads') || '客户线索' }} ({{ leads.length }})</h4>
|
||||
<el-table :data="leads" stripe style="width:100%" @row-click="showLeadDetail">
|
||||
<el-table-column prop="name" :label="$t('agent.leadName') || '公司名称'" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div class="lead-name-cell">
|
||||
<span>{{ row.name }}</span>
|
||||
<el-tag v-if="row.match_score >= 70" size="small" type="success">热</el-tag>
|
||||
<el-tag v-else-if="row.match_score >= 50" size="small" type="warning">温</el-tag>
|
||||
<el-tag v-else size="small" type="info">冷</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="match_score" :label="$t('agent.matchScore') || '匹配度'" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-progress
|
||||
:percentage="row.match_score || 0"
|
||||
:stroke-width="10"
|
||||
:color="scoreColor(row.match_score)"
|
||||
style="width:80px"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="country" :label="$t('agent.country') || '国家'" width="100" />
|
||||
<el-table-column prop="source" :label="$t('agent.source') || '来源'" width="100" />
|
||||
<el-table-column :label="$t('agent.outreach') || '触达文案'" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.outreach"
|
||||
size="small"
|
||||
type="primary"
|
||||
link
|
||||
@click.stop="showOutreach(row)"
|
||||
>{{ $t('agent.preview') || '预览' }}</el-button>
|
||||
<span v-else style="color:#999;font-size:12px">{{ $t('agent.noOutreach') || '未生成' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('agent.actions') || '操作'" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click.stop="goToCustomers(row)">
|
||||
{{ $t('agent.addCustomer') || '添加客户' }}
|
||||
</el-button>
|
||||
<el-button v-if="row.url && row.url.startsWith('http')" size="small" link @click.stop="openUrl(row.url)">
|
||||
{{ $t('agent.visit') || '访问' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- Error message -->
|
||||
<el-alert v-if="selectedPipeline.error_message" :title="selectedPipeline.error_message" type="error" show-icon :closable="false" style="margin-top:12px" />
|
||||
</el-card>
|
||||
|
||||
<!-- Start New Task Dialog -->
|
||||
<el-dialog
|
||||
v-model="showStartDialog"
|
||||
:title="$t('agent.newTask') || '新建 AI 数字员工任务'"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
|
||||
<el-form-item :label="$t('agent.productName') || '产品名称'" prop="product_name">
|
||||
<el-input v-model="form.product_name" :placeholder="$t('agent.productNamePlaceholder') || '例如:户外折叠椅'" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('agent.productDescription') || '产品描述(选填)'" prop="product_description">
|
||||
<el-input
|
||||
v-model="form.product_description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="$t('agent.productDescPlaceholder') || '描述产品的材质、尺寸、优势等,可帮助AI更精准匹配'"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('agent.targetMarket') || '目标市场'" prop="target_market">
|
||||
<el-input v-model="form.target_market" :placeholder="$t('agent.marketPlaceholder') || '例如:美国、德国、东南亚等'" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showStartDialog = false">{{ $t('common.cancel') || '取消' }}</el-button>
|
||||
<el-button type="primary" :loading="starting" @click="startTask">
|
||||
{{ $t('agent.start') || '开始执行' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Outreach Preview Dialog -->
|
||||
<el-dialog
|
||||
v-model="showOutreachDialog"
|
||||
:title="outreachLead?.name"
|
||||
width="600px"
|
||||
>
|
||||
<template v-if="outreachData">
|
||||
<el-tabs>
|
||||
<el-tab-pane label="WhatsApp">
|
||||
<pre class="outreach-text">{{ outreachData.whatsapp_message || '未生成' }}</pre>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="LinkedIn">
|
||||
<pre class="outreach-text">{{ outreachData.linkedin_message || '未生成' }}</pre>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Email">
|
||||
<div class="outreach-email">
|
||||
<div v-if="outreachData.subject" class="outreach-subject"><strong>主题:</strong>{{ outreachData.subject }}</div>
|
||||
<pre class="outreach-text">{{ outreachData.email_body || '未生成' }}</pre>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('agent.tips') || '建议'">
|
||||
<ul v-if="outreachData.tips?.length">
|
||||
<li v-for="(t, i) in outreachData.tips" :key="i" style="margin:4px 0">{{ t }}</li>
|
||||
</ul>
|
||||
<div v-if="outreachData.key_points?.length" style="margin-top:8px">
|
||||
<strong>{{ $t('agent.keyPoints') || '关键要点' }}:</strong>
|
||||
<el-tag v-for="(kp, i) in outreachData.key_points" :key="i" size="small" style="margin:2px">{{ kp }}</el-tag>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="showLeadDialog"
|
||||
:title="leadDetail?.name"
|
||||
width="500px"
|
||||
>
|
||||
<template v-if="leadDetail">
|
||||
<div class="lead-info">
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item :label="$t('agent.matchScore') || '匹配度'">
|
||||
<el-progress :percentage="leadDetail.match_score || 0" :stroke-width="14" :color="scoreColor(leadDetail.match_score)" style="width:120px" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('agent.matchReason') || '匹配理由'">{{ leadDetail.match_reason || '暂无' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('agent.companySummary') || '公司简介'">{{ leadDetail.company_summary || leadDetail.description || '暂无' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('agent.productFit') || '产品契合度'">{{ leadDetail.product_fit || '暂无' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('agent.country') || '国家'">{{ leadDetail.country || '未知' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('agent.source') || '来源'">{{ leadDetail.source || '未知' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="leadDetail.url" :label="'URL'">
|
||||
<a :href="leadDetail.url" target="_blank" rel="noopener">{{ leadDetail.url.substring(0, 50) }}</a>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
Plus, Loading, CircleCheck, CircleClose,
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
startAgentPipeline,
|
||||
listAgentPipelines,
|
||||
getAgentPipeline,
|
||||
} from '@/api'
|
||||
import { createCustomer } from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
// State
|
||||
const showStartDialog = ref(false)
|
||||
const showOutreachDialog = ref(false)
|
||||
const showLeadDialog = ref(false)
|
||||
const starting = ref(false)
|
||||
const loading = ref(false)
|
||||
const pipelines = ref([])
|
||||
const selectedPipeline = ref(null)
|
||||
const selectedId = ref(null)
|
||||
const outreachLead = ref(null)
|
||||
const outreachData = ref(null)
|
||||
const leadDetail = ref(null)
|
||||
const statusFilter = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = 12
|
||||
const totalPipelines = ref(0)
|
||||
|
||||
const form = ref({
|
||||
product_name: '',
|
||||
product_description: '',
|
||||
target_market: '',
|
||||
})
|
||||
|
||||
const rules = {
|
||||
product_name: [{ required: true, message: '请输入产品名称', trigger: 'blur' }],
|
||||
target_market: [{ required: true, message: '请输入目标市场', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
// Computed
|
||||
const totalPages = computed(() => Math.ceil(totalPipelines.value / pageSize))
|
||||
|
||||
const filteredPipelines = computed(() => {
|
||||
if (!statusFilter.value) return pipelines.value
|
||||
return pipelines.value.filter(p => p.status === statusFilter.value)
|
||||
})
|
||||
|
||||
const leads = computed(() => {
|
||||
if (!selectedPipeline.value) return []
|
||||
return selectedPipeline.value.pipeline_data?.leads || []
|
||||
})
|
||||
|
||||
const stats = computed(() => {
|
||||
const all = pipelines.value
|
||||
const completed = all.filter(p => p.status === 'completed')
|
||||
const totalLeads = completed.reduce((sum, p) => sum + (p.pipeline_data?.summary?.total_leads || 0), 0)
|
||||
const saved = completed.reduce((sum, p) => sum + (p.pipeline_data?.summary?.customers_saved || 0), 0)
|
||||
return {
|
||||
total: all.length,
|
||||
completed: completed.length,
|
||||
total_leads: totalLeads,
|
||||
saved_customers: saved,
|
||||
}
|
||||
})
|
||||
|
||||
// Methods
|
||||
function statusTag(status) {
|
||||
return { running: 'warning', completed: 'success', failed: 'danger' }[status] || 'info'
|
||||
}
|
||||
|
||||
function statusLabel(status) {
|
||||
return { running: '进行中', completed: '已完成', failed: '失败', pending: '等待中' }[status] || status
|
||||
}
|
||||
|
||||
function stageLabel(key) {
|
||||
return { discover: '客户搜索', analyze: '匹配分析', outreach: '触达文案', complete: '任务完成' }[key] || key
|
||||
}
|
||||
|
||||
function scoreColor(score) {
|
||||
if (score >= 70) return '#67c23a'
|
||||
if (score >= 50) return '#e6a23c'
|
||||
return '#909399'
|
||||
}
|
||||
|
||||
function formatTime(ts) {
|
||||
if (!ts) return ''
|
||||
const d = new Date(ts)
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
async function loadPipelines() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listAgentPipelines({ page: currentPage.value, size: pageSize })
|
||||
if (res.code === 0) {
|
||||
pipelines.value = res.data.items || []
|
||||
totalPipelines.value = res.data.total || 0
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load pipelines', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function selectPipeline(p) {
|
||||
selectedId.value = p.id
|
||||
try {
|
||||
const res = await getAgentPipeline(p.id)
|
||||
if (res.code === 0) {
|
||||
selectedPipeline.value = res.data
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load pipeline', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function startTask() {
|
||||
const formRef = document.querySelector('.el-form')
|
||||
if (!formRef) return
|
||||
try {
|
||||
await formRef.validate?.()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
starting.value = true
|
||||
try {
|
||||
const res = await startAgentPipeline({
|
||||
product_name: form.value.product_name,
|
||||
product_description: form.value.product_description,
|
||||
target_market: form.value.target_market,
|
||||
})
|
||||
if (res.code === 0) {
|
||||
showStartDialog.value = false
|
||||
form.value = { product_name: '', product_description: '', target_market: '' }
|
||||
await loadPipelines()
|
||||
selectedPipeline.value = res.data
|
||||
selectedId.value = res.data.id
|
||||
currentPage.value = 1
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to start pipeline', e)
|
||||
} finally {
|
||||
starting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function showOutreach(lead) {
|
||||
outreachLead.value = lead
|
||||
outreachData.value = lead.outreach
|
||||
showOutreachDialog.value = true
|
||||
}
|
||||
|
||||
function showLeadDetail(row) {
|
||||
leadDetail.value = row
|
||||
showLeadDialog.value = true
|
||||
}
|
||||
|
||||
async function goToCustomers(lead) {
|
||||
try {
|
||||
await createCustomer({
|
||||
name: lead.name,
|
||||
company: lead.company_summary || lead.name,
|
||||
country: lead.country || '',
|
||||
description: (lead.description || '').substring(0, 500),
|
||||
status: 'lead',
|
||||
source: 'ai_agent',
|
||||
})
|
||||
router.push('/customers')
|
||||
} catch (e) {
|
||||
console.error('Failed to add customer', e)
|
||||
}
|
||||
}
|
||||
|
||||
function openUrl(url) {
|
||||
window.open(url, '_blank', 'noopener')
|
||||
}
|
||||
|
||||
onMounted(loadPipelines)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.agent-dashboard { max-width: 1200px; margin: 0 auto; }
|
||||
.agent-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; gap: 16px; flex-wrap: wrap; }
|
||||
.agent-header-left h2 { margin: 0; font-size: 22px; }
|
||||
.agent-subtitle { margin: 4px 0 0; color: #999; font-size: 13px; }
|
||||
.agent-stats { margin-bottom: 24px; }
|
||||
.stat-item { text-align: center; padding: 8px 0; }
|
||||
.stat-value { font-size: 28px; font-weight: 700; color: #303133; }
|
||||
.stat-label { font-size: 12px; color: #999; margin-top: 4px; }
|
||||
.agent-section { margin-bottom: 24px; }
|
||||
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; flex-wrap: wrap; gap: 8px; }
|
||||
.section-header h3 { margin: 0; font-size: 16px; }
|
||||
.pipeline-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
|
||||
.pipeline-card { cursor: pointer; transition: all 0.2s; }
|
||||
.pipeline-card:hover { transform: translateY(-2px); }
|
||||
.pipeline-card.active { border-color: #409eff; box-shadow: 0 0 0 2px rgba(64,158,255,0.2); }
|
||||
.pipeline-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||||
.pipeline-card-body h4 { margin: 0; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pipeline-market { color: #409eff; font-size: 12px; margin: 4px 0; }
|
||||
.pipeline-summary { color: #666; font-size: 12px; margin: 4px 0; }
|
||||
.pipeline-card-footer { display: flex; justify-content: space-between; font-size: 11px; color: #ccc; margin-top: 8px; }
|
||||
.pipeline-time { color: #999; }
|
||||
.pagination-wrap { display: flex; justify-content: center; margin-top: 20px; }
|
||||
.pipeline-detail { margin-top: 20px; }
|
||||
.detail-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.stage-steps { display: flex; gap: 8px; padding: 16px 0; flex-wrap: wrap; }
|
||||
.stage-step { display: flex; align-items: center; gap: 8px; padding: 8px 14px; border-radius: 8px; background: #f5f7fa; flex: 1; min-width: 140px; }
|
||||
.stage-step.completed { background: #f0f9eb; }
|
||||
.stage-step.running { background: #ecf5ff; }
|
||||
.stage-step.pending { opacity: 0.6; }
|
||||
.stage-icon { flex-shrink: 0; font-size: 20px; }
|
||||
.stage-content { min-width: 0; }
|
||||
.stage-name { font-size: 13px; font-weight: 600; }
|
||||
.stage-msg { font-size: 11px; color: #666; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 180px; }
|
||||
.lead-name-cell { display: flex; align-items: center; gap: 6px; }
|
||||
.lead-name-cell span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 140px; display: inline-block; }
|
||||
.outreach-text { white-space: pre-wrap; word-break: break-word; background: #f5f7fa; padding: 12px; border-radius: 6px; font-size: 13px; line-height: 1.6; max-height: 300px; overflow-y: auto; }
|
||||
.outreach-email .outreach-subject { margin-bottom: 8px; }
|
||||
.lead-info { padding: 4px 0; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pipeline-grid { grid-template-columns: 1fr; }
|
||||
.stage-steps { flex-direction: column; }
|
||||
.agent-header { flex-direction: column; align-items: flex-start; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user