feat: restructure workspace navigation (sidebar 11->5, merged homepage)
Router reorganized: /workspace is now the landing page (NewHome.vue). Sidebar reduced from 11 items to 5 groups (Home/Customers/Products&Quotes/Translate/More). Profile sub-navigation updated to /workspace/profile/*. Legacy discovery/marketing/followup routes preserved as hidden. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -16,17 +16,26 @@
|
||||
: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>
|
||||
<el-menu-item index="/products"><el-icon><Goods /></el-icon><span>{{ $t('nav.products') }}</span></el-menu-item>
|
||||
<el-menu-item index="/quotations"><el-icon><DocumentCopy /></el-icon><span>{{ $t('nav.quotations') }}</span></el-menu-item>
|
||||
<el-menu-item index="/translate"><el-icon><ChatLineSquare /></el-icon><span>{{ $t('nav.translate') }}</span></el-menu-item>
|
||||
<el-menu-item index="/marketing"><el-icon><Promotion /></el-icon><span>{{ $t('nav.marketing') }}</span></el-menu-item>
|
||||
<el-menu-item index="/followup"><el-icon><Message /></el-icon><span>{{ $t('nav.followup') }}</span></el-menu-item>
|
||||
<el-menu-item index="/analytics"><el-icon><DataAnalysis /></el-icon><span>{{ $t('nav.analytics') }}</span></el-menu-item>
|
||||
<el-menu-item index="/team"><el-icon><UserFilled /></el-icon><span>{{ $t('nav.team') }}</span></el-menu-item>
|
||||
<el-menu-item index="/workspace"><el-icon><Odometer /></el-icon><span>{{ $t('nav.home') || '首页工作台' }}</span></el-menu-item>
|
||||
<el-menu-item index="/workspace/customers"><el-icon><User /></el-icon><span>{{ $t('nav.customers') }}</span></el-menu-item>
|
||||
<el-sub-menu index="products-quotations">
|
||||
<template #title>
|
||||
<el-icon><Goods /></el-icon>
|
||||
<span>{{ $t('nav.productsQuotations') || '报价产品' }}</span>
|
||||
</template>
|
||||
<el-menu-item index="/workspace/products"><el-icon><Goods /></el-icon><span>{{ $t('nav.products') }}</span></el-menu-item>
|
||||
<el-menu-item index="/workspace/quotations"><el-icon><DocumentCopy /></el-icon><span>{{ $t('nav.quotations') }}</span></el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-menu-item index="/workspace/translate"><el-icon><ChatLineSquare /></el-icon><span>{{ $t('nav.translate') }}</span></el-menu-item>
|
||||
<el-sub-menu index="more">
|
||||
<template #title>
|
||||
<el-icon><Menu /></el-icon>
|
||||
<span>{{ $t('nav.more') || '更多' }}</span>
|
||||
</template>
|
||||
<el-menu-item index="/workspace/profile"><el-icon><User /></el-icon><span>{{ $t('nav.profile') }}</span></el-menu-item>
|
||||
<el-menu-item index="/workspace/analytics"><el-icon><DataAnalysis /></el-icon><span>{{ $t('nav.analytics') }}</span></el-menu-item>
|
||||
<el-menu-item index="/workspace/team"><el-icon><UserFilled /></el-icon><span>{{ $t('nav.team') }}</span></el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</aside>
|
||||
|
||||
@@ -36,17 +45,17 @@
|
||||
<el-icon :size="20"><Expand /></el-icon>
|
||||
</el-button>
|
||||
<el-breadcrumb separator="/" class="breadcrumb">
|
||||
<el-breadcrumb-item :to="'/workspace'">{{ $t('nav.workspace') }}</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-if="route.meta?.title" :to="route.path">{{ $t('nav.' + route.name?.toLowerCase()) || route.meta.title }}</el-breadcrumb-item>
|
||||
<el-breadcrumb-item :to="'/workspace'">{{ $t('nav.home') || '首页工作台' }}</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-if="route.meta?.title && route.path !== '/workspace'" :to="route.path">{{ $t('nav.' + route.name?.toLowerCase()) || route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
<div class="topbar-right">
|
||||
<el-button text style="font-size:13px;color:#999" @click="toggleLang">{{ currentLang }}</el-button>
|
||||
<el-button v-if="creditBalance !== null" text class="credit-btn" @click="$router.push('/credits')">
|
||||
<el-button v-if="creditBalance !== null" text class="credit-btn" @click="$router.push('/workspace/profile/credits')">
|
||||
<el-icon><Coin /></el-icon>
|
||||
<span class="credit-text">{{ creditBalance }} {{ $t('topbar.credits') }}</span>
|
||||
</el-button>
|
||||
<el-badge :value="unread" :hidden="!unread" class="notif-badge">
|
||||
<el-button text style="font-size:18px" @click="$router.push('/notifications')">
|
||||
<el-button text style="font-size:18px" @click="$router.push('/workspace/profile/notifications')">
|
||||
<el-icon><Bell /></el-icon>
|
||||
</el-button>
|
||||
</el-badge>
|
||||
@@ -58,8 +67,8 @@
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="$router.push('/profile')">{{ $t('nav.profile') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="$router.push('/notifications')">{{ $t('nav.notifications') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="$router.push('/workspace/profile')">{{ $t('nav.profile') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="$router.push('/workspace/profile/notifications')">{{ $t('nav.notifications') }}</el-dropdown-item>
|
||||
<el-dropdown-item divided @click="handleLogout">{{ $t('common.confirm') }}退出</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
|
||||
@@ -3,52 +3,49 @@ 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'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Workspace', component: () => import('@/views/Workspace.vue'), meta: { title: '工作台' } },
|
||||
{ path: '', name: 'Home', component: () => import('@/views/NewHome.vue'), meta: { title: '首页工作台' } },
|
||||
{ path: 'customers', name: 'Customers', component: () => import('@/views/Customers.vue'), meta: { title: '客户管理' } },
|
||||
{ path: 'products', name: 'Products', component: () => import('@/views/Products.vue'), meta: { title: '产品库' } },
|
||||
{ path: 'quotations', name: 'Quotations', component: () => import('@/views/Quotations.vue'), meta: { title: '报价单' } },
|
||||
{ path: 'translate', name: 'Translate', component: () => import('@/views/Translate.vue'), meta: { title: '智能翻译' } },
|
||||
{ path: 'analytics', name: 'Analytics', component: () => import('@/views/Analytics.vue'), meta: { title: '数据分析' } },
|
||||
{ path: 'team', name: 'Team', component: () => import('@/views/Team.vue'), meta: { title: '团队协作' } },
|
||||
{ path: 'profile', name: 'Profile', component: () => import('@/views/Profile.vue'), meta: { title: '个人中心' } },
|
||||
{ path: 'profile/credits', name: 'Credits', component: () => import('@/views/Credits.vue'), meta: { title: '购买次数' } },
|
||||
{ path: 'profile/certification', name: 'Certification', component: () => import('@/views/Certification.vue'), meta: { title: '实名认证' } },
|
||||
{ path: 'profile/invoice', name: 'Invoice', component: () => import('@/views/Invoice.vue'), meta: { title: '发票管理' } },
|
||||
{ path: 'profile/notifications', name: 'Notifications', component: () => import('@/views/Notifications.vue'), meta: { title: '通知中心' } },
|
||||
{ path: 'profile/feedback', name: 'Feedback', component: () => import('@/views/Feedback.vue'), meta: { title: '意见反馈' } },
|
||||
]
|
||||
},
|
||||
// Redirect old top-level routes to new /workspace children
|
||||
{ path: '/customers', redirect: '/workspace/customers' },
|
||||
{ path: '/products', redirect: '/workspace/products' },
|
||||
{ path: '/quotations', redirect: '/workspace/quotations' },
|
||||
{ path: '/translate', redirect: '/workspace/translate' },
|
||||
{ path: '/analytics', redirect: '/workspace/analytics' },
|
||||
{ path: '/team', redirect: '/workspace/team' },
|
||||
{ path: '/profile', redirect: '/workspace/profile' },
|
||||
{ path: '/credits', redirect: '/workspace/profile/credits' },
|
||||
{ path: '/certification', redirect: '/workspace/profile/certification' },
|
||||
{ path: '/invoice', redirect: '/workspace/profile/invoice' },
|
||||
{ path: '/notifications', redirect: '/workspace/profile/notifications' },
|
||||
{ path: '/feedback', redirect: '/workspace/profile/feedback' },
|
||||
{ path: '/agent', redirect: '/workspace' },
|
||||
{ path: '/workspace/home', redirect: '/workspace' },
|
||||
|
||||
// Hidden routes – still accessible via direct URL but not in sidebar
|
||||
{
|
||||
path: '/translate',
|
||||
path: '/discovery',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Translate', component: () => import('@/views/Translate.vue'), meta: { title: '智能翻译' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/customers',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Customers', component: () => import('@/views/Customers.vue'), meta: { title: '客户管理' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/products',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Products', component: () => import('@/views/Products.vue'), meta: { title: '产品库' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/quotations',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Quotations', component: () => import('@/views/Quotations.vue'), meta: { title: '报价单' } },
|
||||
{ path: '', name: 'Discovery', component: () => import('@/views/Discovery.vue'), meta: { title: '发现客户' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -59,14 +56,6 @@ const routes = [
|
||||
{ path: '', name: 'Marketing', component: () => import('@/views/Marketing.vue'), meta: { title: '营销素材' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/discovery',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Discovery', component: () => import('@/views/Discovery.vue'), meta: { title: '发现客户' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/followup',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
@@ -75,70 +64,6 @@ const routes = [
|
||||
{ path: '', name: 'Followup', component: () => import('@/views/Followup.vue'), meta: { title: '智能跟进' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/analytics',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Analytics', component: () => import('@/views/Analytics.vue'), meta: { title: '数据分析' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/team',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Team', component: () => import('@/views/Team.vue'), meta: { title: '团队协作' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/notifications',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Notifications', component: () => import('@/views/Notifications.vue'), meta: { title: '通知中心' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Profile', component: () => import('@/views/Profile.vue'), meta: { title: '个人中心' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/credits',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Credits', component: () => import('@/views/Credits.vue'), meta: { title: '购买次数' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/certification',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Certification', component: () => import('@/views/Certification.vue'), meta: { title: '实名认证' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/invoice',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Invoice', component: () => import('@/views/Invoice.vue'), meta: { title: '发票管理' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/feedback',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Feedback', component: () => import('@/views/Feedback.vue'), meta: { title: '意见反馈' } },
|
||||
]
|
||||
},
|
||||
{ path: '/:pathMatch(.*)*', redirect: '/' },
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,679 @@
|
||||
<template>
|
||||
<div class="home-dashboard">
|
||||
<!-- AI Input Bar (Wave 2 placeholder) -->
|
||||
<div class="ai-input-section">
|
||||
<div class="ai-input-bar">
|
||||
<el-icon class="ai-input-icon" :size="20"><MagicStick /></el-icon>
|
||||
<el-input
|
||||
v-model="aiInput"
|
||||
:placeholder="$t('home.aiPlaceholder') || '描述你的需求,AI 会帮你完成...'"
|
||||
size="large"
|
||||
clearable
|
||||
@keyup.enter="handleAiInput"
|
||||
/>
|
||||
<el-button type="primary" size="large" @click="handleAiInput">{{ $t('home.send') || '发送' }}</el-button>
|
||||
</div>
|
||||
<div class="quick-tasks">
|
||||
<el-button size="small" round @click="quickTask('find')">
|
||||
<el-icon><Search /></el-icon>{{ $t('home.findCustomers') || '找客户' }}
|
||||
</el-button>
|
||||
<el-button size="small" round @click="quickTask('write')">
|
||||
<el-icon><Edit /></el-icon>{{ $t('home.writeCopy') || '写文案' }}
|
||||
</el-button>
|
||||
<el-button size="small" round @click="quickTask('translate')">
|
||||
<el-icon><ChatLineSquare /></el-icon>{{ $t('home.translate') || '翻译' }}
|
||||
</el-button>
|
||||
<el-button size="small" round @click="quickTask('quote')">
|
||||
<el-icon><DocumentCopy /></el-icon>{{ $t('home.quote') || '报价' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Welcome + Stats -->
|
||||
<div class="welcome-section">
|
||||
<div class="welcome-left">
|
||||
<h2>{{ $t('home.greeting') || '你好' }},{{ auth.user?.username || '用户' }}</h2>
|
||||
<p class="welcome-desc">{{ $t('home.subtitle') || 'AI 数字员工为你自动挖掘、分析、触达、跟进' }}</p>
|
||||
</div>
|
||||
<div class="welcome-right">
|
||||
<el-button type="primary" size="large" @click="showStartDialog = true" :icon="Plus">
|
||||
{{ $t('agent.newTask') || '新建任务' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Credit Balance -->
|
||||
<el-card shadow="never" class="credit-card">
|
||||
<div class="credit-inner">
|
||||
<div class="credit-left">
|
||||
<div class="credit-label">{{ $t('home.creditBalance') || '信用余额' }}</div>
|
||||
<div class="credit-amount">{{ creditBalance ?? '...' }} <small>{{ $t('home.credits') || '次' }}</small></div>
|
||||
</div>
|
||||
<div class="credit-right">
|
||||
<el-button type="primary" @click="$router.push('/workspace/profile/credits')">{{ $t('home.buyCredits') || '购买次数' }} →</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<el-row :gutter="16" class="stats-row">
|
||||
<el-col :xs="12" :sm="6" v-for="item in stats" :key="item.label">
|
||||
<el-card shadow="hover" class="stat-card" @click="item.route && $router.push(item.route)">
|
||||
<div class="stat-value" :style="{ color: item.color }">{{ item.value }}</div>
|
||||
<div class="stat-label">{{ item.label }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- Pipeline Stats (from Agent.vue) -->
|
||||
<el-row :gutter="16" class="agent-stats">
|
||||
<el-col :xs="12" :sm="3" v-for="s in pipelineStats" :key="s.label">
|
||||
<el-card shadow="never">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" :style="{ color: s.color }">{{ s.value }}</div>
|
||||
<div class="stat-label">{{ s.label }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- Activity Feed (Wave 2 placeholder) -->
|
||||
<el-card shadow="never" class="activity-section" v-if="recentActivities.length">
|
||||
<template #header>
|
||||
<span class="section-title">{{ $t('home.recentActivity') || '最近活动' }}</span>
|
||||
</template>
|
||||
<div class="activity-list">
|
||||
<div v-for="act in recentActivities" :key="act.id" class="activity-item">
|
||||
<el-tag :type="act.type" size="small" class="activity-tag">{{ act.tag }}</el-tag>
|
||||
<span class="activity-text">{{ act.text }}</span>
|
||||
<span class="activity-time">{{ act.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 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">
|
||||
{{ $t('agent.foundLeads') || '发现' }}
|
||||
<strong>{{ p.pipeline_data.summary.total_leads || 0 }}</strong>
|
||||
{{ $t('agent.leads') || '个线索' }},
|
||||
{{ $t('agent.highMatch') || '高匹配' }}
|
||||
<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>
|
||||
<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>
|
||||
<el-button size="small" @click="selectedPipeline = null">{{ $t('common.close') || '关闭' }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
||||
<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">{{ $t('agent.hot') || '热' }}</el-tag>
|
||||
<el-tag v-else-if="row.match_score >= 50" size="small" type="warning">{{ $t('agent.warm') || '温' }}</el-tag>
|
||||
<el-tag v-else size="small" type="info">{{ $t('agent.cold') || '冷' }}</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>
|
||||
|
||||
<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="$t('agent.whatsapp') || '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="$t('agent.email') || 'Email'">
|
||||
<div class="outreach-email">
|
||||
<div v-if="outreachData.subject" class="outreach-subject"><strong>{{ $t('agent.subject') || '主题' }}:</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 { useAuthStore } from '@/stores/auth'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Plus, MagicStick, Search, Edit, ChatLineSquare, DocumentCopy, Loading, CircleCheck, CircleClose } from '@element-plus/icons-vue'
|
||||
import {
|
||||
startAgentPipeline,
|
||||
listAgentPipelines,
|
||||
getAgentPipeline,
|
||||
createCustomer,
|
||||
getCreditBalance,
|
||||
getAnalyticsOverview,
|
||||
} from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const auth = useAuthStore()
|
||||
|
||||
// AI Input
|
||||
const aiInput = ref('')
|
||||
function handleAiInput() {
|
||||
if (!aiInput.value.trim()) return
|
||||
const text = aiInput.value.trim()
|
||||
// Route to Agent pipeline or translate based on intent
|
||||
if (text.includes('翻译') || text.includes('translate')) {
|
||||
router.push('/workspace/translate')
|
||||
} else {
|
||||
// Default: treat as Agent task intent
|
||||
showStartDialog.value = true
|
||||
form.value.product_name = text
|
||||
}
|
||||
aiInput.value = ''
|
||||
}
|
||||
|
||||
function quickTask(type) {
|
||||
const prompts = {
|
||||
find: '帮我找____市场的____产品买家',
|
||||
write: '为产品____写一篇____风格的____',
|
||||
translate: '',
|
||||
quote: '根据客户____创建报价单',
|
||||
}
|
||||
if (type === 'translate') {
|
||||
router.push('/workspace/translate')
|
||||
} else {
|
||||
showStartDialog.value = true
|
||||
form.value.product_name = prompts[type] || ''
|
||||
}
|
||||
}
|
||||
|
||||
// Credit balance
|
||||
const creditBalance = ref(null)
|
||||
|
||||
// Stats from analytics
|
||||
const stats = ref([])
|
||||
|
||||
// Pipeline 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 pipelineStats = computed(() => {
|
||||
const all = pipelines.value
|
||||
const completedCount = all.filter(p => p.status === 'completed')
|
||||
const totalLeads = completedCount.reduce((sum, p) => sum + (p.pipeline_data?.summary?.total_leads || 0), 0)
|
||||
const saved = completedCount.reduce((sum, p) => sum + (p.pipeline_data?.summary?.customers_saved || 0), 0)
|
||||
return [
|
||||
{ value: all.length, label: t('agent.totalTasks') || '总任务', color: '#303133' },
|
||||
{ value: completedCount.length, label: t('agent.completed') || '已完成', color: '#67c23a' },
|
||||
{ value: totalLeads, label: t('agent.totalLeads') || '累计线索', color: '#e6a23c' },
|
||||
{ value: saved, label: t('agent.savedCustomers') || '已保存客户', color: '#409eff' },
|
||||
]
|
||||
})
|
||||
|
||||
// Recent activities (from pipelines)
|
||||
const recentActivities = computed(() => {
|
||||
const acts = []
|
||||
const recent = [...pipelines.value].sort((a, b) => new Date(b.created_at) - new Date(a.created_at)).slice(0, 5)
|
||||
for (const p of recent) {
|
||||
if (p.status === 'completed') {
|
||||
acts.push({
|
||||
id: `p-${p.id}`,
|
||||
type: 'success',
|
||||
tag: 'Agent',
|
||||
text: `${p.product_name} — ${p.target_market},发现 ${p.pipeline_data?.summary?.total_leads || 0} 个线索`,
|
||||
time: formatTime(p.created_at),
|
||||
})
|
||||
} else if (p.status === 'running') {
|
||||
acts.push({
|
||||
id: `p-${p.id}`,
|
||||
type: 'warning',
|
||||
tag: 'Agent',
|
||||
text: `${p.product_name} — ${p.target_market},执行中 ${p.progress || 0}%`,
|
||||
time: formatTime(p.created_at),
|
||||
})
|
||||
}
|
||||
}
|
||||
return acts
|
||||
})
|
||||
|
||||
// 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('/workspace/customers')
|
||||
} catch (e) {
|
||||
console.error('Failed to add customer', e)
|
||||
}
|
||||
}
|
||||
|
||||
function openUrl(url) {
|
||||
window.open(url, '_blank', 'noopener')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loadPipelines()
|
||||
try {
|
||||
const balance = await getCreditBalance()
|
||||
creditBalance.value = balance.balance
|
||||
} catch { creditBalance.value = null }
|
||||
try {
|
||||
const res = await getAnalyticsOverview()
|
||||
const d = res?.data || res || {}
|
||||
stats.value = [
|
||||
{ value: d.customers?.total || 0, label: t('home.totalCustomers') || '客户总数', color: '#1890ff', route: '/workspace/customers' },
|
||||
{ value: d.translations?.today || 0, label: t('home.todayTranslate') || '今日翻译', color: '#52c41a', route: '/workspace/translate' },
|
||||
{ value: d.quotations?.total || 0, label: t('home.totalQuotations') || '报价单数', color: '#faad14', route: '/workspace/quotations' },
|
||||
{ value: pipelines.value.filter(p => p.status === 'running').length, label: t('home.runningTasks') || '执行中', color: '#e6a23c' },
|
||||
]
|
||||
} catch { /* ignore */ }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-dashboard { max-width: 1200px; margin: 0 auto; }
|
||||
|
||||
/* AI Input Bar */
|
||||
.ai-input-section { margin-bottom: 24px; }
|
||||
.ai-input-bar { display: flex; gap: 12px; align-items: center; background: #fff; padding: 16px 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
|
||||
.ai-input-icon { color: #722ed1; flex-shrink: 0; }
|
||||
.ai-input-bar :deep(.el-input) { flex: 1; }
|
||||
.ai-input-bar :deep(.el-input__wrapper) { box-shadow: none !important; border: 1px solid #e8e8e8; border-radius: 8px; }
|
||||
.ai-input-bar :deep(.el-input__wrapper:hover) { border-color: #722ed1; }
|
||||
.quick-tasks { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; padding: 0 4px; }
|
||||
.quick-tasks .el-button { color: #666; border-color: #e8e8e8; }
|
||||
.quick-tasks .el-button:hover { color: #722ed1; border-color: #722ed1; }
|
||||
|
||||
/* Welcome */
|
||||
.welcome-section { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.welcome-left h2 { font-size: 22px; font-weight: 700; color: #333; margin: 0 0 4px; }
|
||||
.welcome-desc { font-size: 13px; color: #999; margin: 0; }
|
||||
.welcome-right { display: flex; gap: 12px; flex-shrink: 0; }
|
||||
|
||||
/* Credit */
|
||||
.credit-card { margin-bottom: 16px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; }
|
||||
.credit-inner { display: flex; align-items: center; justify-content: space-between; padding: 4px 0; }
|
||||
.credit-label { font-size: 13px; opacity: 0.9; }
|
||||
.credit-amount { font-size: 28px; font-weight: 700; margin-top: 4px; }
|
||||
.credit-amount small { font-size: 14px; font-weight: 400; opacity: 0.8; }
|
||||
|
||||
/* Stats */
|
||||
.stats-row { margin-bottom: 20px; }
|
||||
.stat-card { cursor: pointer; text-align: center; transition: all 0.25s; }
|
||||
.stat-card:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,0.08); }
|
||||
.stat-value { font-size: 28px; font-weight: 700; }
|
||||
.stat-label { font-size: 12px; color: #999; margin-top: 4px; }
|
||||
|
||||
.agent-stats { margin-bottom: 24px; }
|
||||
.stat-item { text-align: center; padding: 8px 0; }
|
||||
|
||||
/* Activity */
|
||||
.activity-section { margin-bottom: 24px; }
|
||||
.section-title { font-weight: 600; font-size: 15px; }
|
||||
.activity-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.activity-item { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid #f5f5f5; }
|
||||
.activity-item:last-child { border-bottom: none; }
|
||||
.activity-tag { flex-shrink: 0; }
|
||||
.activity-text { flex: 1; font-size: 13px; color: #333; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.activity-time { font-size: 11px; color: #999; flex-shrink: 0; }
|
||||
|
||||
/* Pipeline */
|
||||
.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; flex-wrap: wrap; gap: 8px; }
|
||||
.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; }
|
||||
.welcome-section { flex-direction: column; align-items: flex-start; gap: 12px; }
|
||||
.ai-input-bar { flex-direction: column; }
|
||||
.ai-input-bar :deep(.el-input) { width: 100%; }
|
||||
.welcome-left h2 { font-size: 20px; }
|
||||
}
|
||||
</style>
|
||||
@@ -11,19 +11,22 @@
|
||||
</div>
|
||||
<el-divider style="margin:8px 0" />
|
||||
<div class="profile-menu">
|
||||
<div class="menu-item" @click="$router.push('/credits')">
|
||||
<div class="menu-item" :class="{ active: $route.path === '/workspace/profile' }" @click="$router.push('/workspace/profile')">
|
||||
<el-icon><User /></el-icon><span>个人资料</span>
|
||||
</div>
|
||||
<div class="menu-item" :class="{ active: $route.path === '/workspace/profile/credits' }" @click="$router.push('/workspace/profile/credits')">
|
||||
<el-icon><Coin /></el-icon><span>购买次数</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="$router.push('/certification')">
|
||||
<div class="menu-item" :class="{ active: $route.path === '/workspace/profile/certification' }" @click="$router.push('/workspace/profile/certification')">
|
||||
<el-icon><Stamp /></el-icon><span>实名认证</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="$router.push('/invoice')">
|
||||
<div class="menu-item" :class="{ active: $route.path === '/workspace/profile/invoice' }" @click="$router.push('/workspace/profile/invoice')">
|
||||
<el-icon><List /></el-icon><span>发票管理</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="$router.push('/notifications')">
|
||||
<div class="menu-item" :class="{ active: $route.path === '/workspace/profile/notifications' }" @click="$router.push('/workspace/profile/notifications')">
|
||||
<el-icon><Bell /></el-icon><span>通知中心</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="$router.push('/feedback')">
|
||||
<div class="menu-item" :class="{ active: $route.path === '/workspace/profile/feedback' }" @click="$router.push('/workspace/profile/feedback')">
|
||||
<el-icon><ChatDotSquare /></el-icon><span>意见反馈</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,6 +73,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { updateProfile, changePassword } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const auth = useAuthStore()
|
||||
const saving = ref(false)
|
||||
const showPassword = ref(false)
|
||||
@@ -115,4 +119,5 @@ async function changePw() {
|
||||
.profile-menu { padding: 0; }
|
||||
.menu-item { display: flex; align-items: center; gap: 10px; padding: 12px 16px; cursor: pointer; border-radius: 6px; transition: background 0.2s; }
|
||||
.menu-item:hover { background: #f0f5ff; color: #409eff; }
|
||||
.menu-item.active { background: #e6f7ff; color: #1890ff; font-weight: 500; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user