@@ -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 : 1200 px ; margin : 0 auto ; }
. agent - header { display : flex ; justify - content : space - between ; align - items : center ; margin - bottom : 20 px ; gap : 16 px ; flex - wrap : wrap ; }
. agent - header - left h2 { margin : 0 ; font - size : 22 px ; }
. agent - subtitle { margin : 4 px 0 0 ; color : # 999 ; font - size : 13 px ; }
. agent - stats { margin - bottom : 24 px ; }
. stat - item { text - align : center ; padding : 8 px 0 ; }
. stat - value { font - size : 28 px ; font - weight : 700 ; color : # 303133 ; }
. stat - label { font - size : 12 px ; color : # 999 ; margin - top : 4 px ; }
. agent - section { margin - bottom : 24 px ; }
. section - header { display : flex ; justify - content : space - between ; align - items : center ; margin - bottom : 16 px ; flex - wrap : wrap ; gap : 8 px ; }
. section - header h3 { margin : 0 ; font - size : 16 px ; }
. pipeline - grid { display : grid ; grid - template - columns : repeat ( auto - fill , minmax ( 280 px , 1 fr ) ) ; gap : 12 px ; }
. pipeline - card { cursor : pointer ; transition : all 0.2 s ; }
. pipeline - card : hover { transform : translateY ( - 2 px ) ; }
. pipeline - card . active { border - color : # 409 eff ; box - shadow : 0 0 0 2 px rgba ( 64 , 158 , 255 , 0.2 ) ; }
. pipeline - card - header { display : flex ; justify - content : space - between ; align - items : center ; margin - bottom : 8 px ; }
. pipeline - card - body h4 { margin : 0 ; font - size : 15 px ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
. pipeline - market { color : # 409 eff ; font - size : 12 px ; margin : 4 px 0 ; }
. pipeline - summary { color : # 666 ; font - size : 12 px ; margin : 4 px 0 ; }
. pipeline - card - footer { display : flex ; justify - content : space - between ; font - size : 11 px ; color : # ccc ; margin - top : 8 px ; }
. pipeline - time { color : # 999 ; }
. pagination - wrap { display : flex ; justify - content : center ; margin - top : 20 px ; }
. pipeline - detail { margin - top : 20 px ; }
. detail - header { display : flex ; justify - content : space - between ; align - items : center ; }
. stage - steps { display : flex ; gap : 8 px ; padding : 16 px 0 ; flex - wrap : wrap ; }
. stage - step { display : flex ; align - items : center ; gap : 8 px ; padding : 8 px 14 px ; border - radius : 8 px ; background : # f5f7fa ; flex : 1 ; min - width : 140 px ; }
. stage - step . completed { background : # f0f9eb ; }
. stage - step . running { background : # ecf5ff ; }
. stage - step . pending { opacity : 0.6 ; }
. stage - icon { flex - shrink : 0 ; font - size : 20 px ; }
. stage - content { min - width : 0 ; }
. stage - name { font - size : 13 px ; font - weight : 600 ; }
. stage - msg { font - size : 11 px ; color : # 666 ; margin - top : 2 px ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; max - width : 180 px ; }
. lead - name - cell { display : flex ; align - items : center ; gap : 6 px ; }
. lead - name - cell span { overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; max - width : 140 px ; display : inline - block ; }
. outreach - text { white - space : pre - wrap ; word - break : break - word ; background : # f5f7fa ; padding : 12 px ; border - radius : 6 px ; font - size : 13 px ; line - height : 1.6 ; max - height : 300 px ; overflow - y : auto ; }
. outreach - email . outreach - subject { margin - bottom : 8 px ; }
. lead - info { padding : 4 px 0 ; }
@ media ( max - width : 768 px ) {
. pipeline - grid { grid - template - columns : 1 fr ; }
. stage - steps { flex - direction : column ; }
. agent - header { flex - direction : column ; align - items : flex - start ; }
}
< / style >