Compare commits

..

4 Commits

Author SHA1 Message Date
wlt 5e1aa95969 docs: add workspace UX redesign plan
Comprehensive UX design document covering: current state analysis, design principles (minimal/functional/AI-first), navigation restructuring plan, new dashboard design, page-level AI enhancements, and phased migration strategy.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-17 10:48:30 +08:00
wlt caa484f651 feat: add agent mode toggle to AiAssistant + i18n nav keys
AiAssistant: new mode switch (AI助手/数字员工) in dialog header. Agent mode shows a product info form that triggers the agent pipeline (search->analyze->outreach). Pipeline progress stages and lead results displayed inline. i18n: added missing nav keys (home, more, productsQuotations) to both zh-CN and en locale files.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-17 10:48:25 +08:00
wlt 5590f81536 feat: AI enhancements on translate/customers/analytics pages
Translate: auto-detect source language toggle, one-click reply button that auto-fills inquiry and generates suggestions. Customers: AI suggested action column with smart heuristics based on status/health/last_contact. Analytics: AI-generated analysis summary showing key insights (customer count, top market, daily activity).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-17 10:48:19 +08:00
wlt 45e98a9c82 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>
2026-06-17 10:48:13 +08:00
11 changed files with 1240 additions and 134 deletions
+265
View File
@@ -0,0 +1,265 @@
# TradeMate 工作台极简改造设计方案 (V2)
> 版本: v1.0 | 创建日期: 2026-06-17 | 状态: 待审核
---
## 1. 现状分析与痛点评级
### 1.1 导航架构现状
当前 `user-frontend`**18 个路由**:侧边栏 11 项、个人中心子菜单 5 项、独立页面 2 个。
**侧边栏(11 项)**:
| 路由 | 页面 | 图标 | 说明 |
|------|------|------|------|
| /agent | AI数字员工 | MagicStick | Agent Orchestrator 仪表盘 |
| /discovery | 发现客户 | Search | Google 搜索+联系人提取 |
| /workspace | 工作台 | Odometer | 统计+快捷入口+活动 |
| /customers | 客户管理 | User | CRUD |
| /products | 产品库 | Goods | CRUD |
| /quotations | 报价单 | DocumentCopy | CRUD + AI 智能报价 |
| /translate | 智能翻译 | ChatLineSquare | 翻译+回复+提取 |
| /marketing | 营销素材 | Promotion | AI 文案生成 |
| /followup | 智能跟进 | Message | 沉默检测+建议 |
| /analytics | 数据分析 | DataAnalysis | 统计+图表 |
| /team | 团队协作 | UserFilled | 团队管理 |
**个人中心子菜单(Dropdown → 5 项)**: 购买次数 / 实名认证 / 发票管理 / 通知中心 / 意见反馈
### 1.2 核心问题
| 等级 | 问题 | 影响 |
|------|------|------|
| 🔴 P1 | 侧边栏 11 项过载 | 用户决策瘫痪,不知从何开始 |
| 🔴 P2 | 工作台仪表盘 8+ 模块堆砌 | 核心任务被淹没 |
| 🔴 P3 | AI数字员工与 Discovery/Marketing/Followup 功能重叠 | 用户困惑该用哪个 |
| 🟡 P4 | AiAssistant 浮动聊天与工作台脱节 | 需反复粘贴文字 |
| 🟡 P5 | 快捷翻译在 Workspace 和 Translate 重复 | 维护成本+体验不一致 |
| 🟢 P6 | 各页面无统一空状态引导 | 新用户不知所措 |
### 1.3 功能重叠分析
```
Agent.vue → 产品发现(重复Discovery) + 营销文案(重复Marketing) + 跟进分析(重复Followup)
Workspace.vue → 快捷翻译(重复Translate) + 跟进提醒(重复Followup)
```
---
## 2. 设计原则
### 2.1 AI-First,而非 Feature-First
AI 数字员工从"一个页面"升级为工作台核心入口。用户在 AI 输入栏发起任务,系统自动路由。特征功能(翻译、跟进、营销)后退为 AI 的工具箱。
### 2.2 任务驱动,而非页面驱动
统一输入栏取代分散表单。"帮我找美国市场做户外折叠椅的买家"→ 一步触发 Agent 完整工作流。
### 2.3 Progressive Disclosure(渐进展示)
首屏只展示:输入栏 + 核心 3 数据 + 下一步建议。卡片默认收起,展开才显示详情。
### 2.4 零认知摩擦
导航不超过 5 项。次级入口通过"更多"折叠。操作提示用自然语言。
### 2.5 嵌入式 AI,而非分离式 Chat
翻译中 AI 自动感知文本。客户详情中 AI 生成跟进话术。报价单中 AI 分析询盘。
---
## 3. 改造目标
| 指标 | 当前 | 目标 |
|------|------|------|
| 侧边栏项数 | 11 | ≤ 5 |
| 首屏模块数 | 8+ | ≤ 5 |
| 新用户首步完成 | 自行探索 | ≤ 3 秒 |
| 冗余页面 | 3 处 | 0 处 |
### 改造范围
- **Scope A (必做)**: 导航 11→5 + Profile 侧边栏化
- **Scope B (必做)**: Agent 升级为工作台首页 + 统一输入栏
- **Scope C (推荐)**: Discovery/Marketing/Followup → Agent 工作流
- **Scope D (推荐)**: 各页面 AI 嵌入 + 空状态引导
---
## 4. 导航架构重构
### 4.1 新侧边栏设计
| 图标 | 名称 | 说明 |
|------|------|------|
| 🏠 | 首页工作台 | Agent + 仪表盘合二为一 |
| 👥 | 客户管理 | 保持不变 |
| 📄 | 报价产品 | Products + Quotations 合并导航 |
| 🌐 | 智能翻译 | 保持不变 |
| ☰ | 更多 | Analytics / Team / 个人设置 |
**要点**:
- `AI数字员工` + `工作台`**首页工作台**Agent.vue 改造为新首页)
- `发现客户` + `营销素材` + `智能跟进` → 嵌入 Agent 工作流,不再独立导航
- `产品库` + `报价单` → 合并为"报价产品"
- Discovery/Marketing/Followup 保留路由但不在导航显示(向后兼容)
### 4.2 新路由架构
```
/workspace/
├── (空) → NewHome.vue(原 Agent.vue 改造)
├── customers → 客户管理
├── products → 产品库
├── quotations → 报价单
├── translate → 智能翻译
├── analytics → 数据分析
├── team → 团队协作
└── profile → 个人中心(含所有子页面侧边栏)
```
---
## 5. 新仪表盘设计
### 5.1 页面布局
```
┌── AI 输入栏 ──────────────────────────────────────────┐
│ [💡 描述你的需求,AI 会帮你完成... ] │
│ 快捷: [🔍 找客户] [📝 写文案] [💬 翻译] [📋 报价] │
└─────────────────────────────────────────────────────────┘
┌── 今日核心 ──────────────────────────────────────────┐
│ 待跟进: 5 新客户: 3 今日翻译: 12 │
└─────────────────────────────────────────────────────────┘
┌── 最近活动 ──────────────────────────────────────────┐
│ [Agent] 为 户外折叠椅 找到 15 个美国潜在买家 │
│ [Agent] 已发送 3 封跟进邮件给墨西哥客户 │
│ [翻译] 翻译了 2 段 WhatsApp 消息 │
│ [系统] 客户 Carlos 已沉默 5 天 │
└─────────────────────────────────────────────────────────┘
┌── 快捷入口 ──────────────────────────────────────────┐
│ [👥 客户管理] [📄 报价产品] [🌐 翻译] [📊 分析] │
└─────────────────────────────────────────────────────────┘
```
### 5.2 AI 输入栏(核心交互)
- 进入工作台自动聚焦,`Cmd+K`/`Ctrl+K` 随时唤起
- 自然语言指令自动识别意图:
- "帮我找美国市场做户外折叠椅的买家" → Agent Pipeline
- "给上周报价的墨西哥客户写一封跟进邮件" → Agent 跟进
- "翻译这段文本:..." → 翻译工具
- "展示上个月的客户情况分析" → 分析页
- 输入历史可回溯,支持追问
### 5.3 组件化设计
```
NewHome.vue
├── AiInputBar.vue # AI 统一输入栏
│ ├── QuickTasks.vue # 快捷任务按钮
│ └── IntentDetector.js # 意图识别
├── CoreStats.vue # 今日核心
├── ActivityFeed.vue # 最近活动
│ ├── ActivityItem.vue
│ └── ActivityEmpty.vue
└── QuickAccess.vue # 快捷入口
```
---
## 6. 页面级改造
| 页面 | 现状 | 改造 |
|------|------|------|
| Translate.vue | 三段式需手动切换 | AI 自动识别语言 + "一键回复"联动 Agent |
| Customers.vue | 标准 CRUD | AI 批量操作 + "建议动作"列 |
| Products+Quotations | 独立 | "产品→报价"一键流转 + AI 自动填充 |
| Analytics.vue | 静态统计 | AI 分析摘要 + 交互图表 + 趋势线 |
| Agent.vue | 独立仪表盘 | 升级为 NewHome.vue (首页) |
---
## 7. 要素合并
| 现状 | 改造后 |
|------|--------|
| AiAssistant 浮动气泡 | Agent 对话模式,可执行操作+感知上下文 |
| Discovery 独立页 | Agent Pipeline 步骤(自动搜索→分析→评分) |
| Marketing 独立页 | Agent 生成能力,自动读取产品库 |
| Followup 独立页 | Agent 后台任务,活动流确认即可 |
---
## 8. 迁移计划
### 阶段一:导航精简 + Agent 首页化(2-3 天)
```
1. 侧边栏 11 → 5 项
2. Agent.vue 迁移到 /workspace
3. Workspace.vue 内容并入
4. Profile 侧边栏子菜单
```
涉及: UserLayout.vue, router/index.js, Agent.vue, Workspace.vue, Profile.vue
### 阶段二:AI 输入栏 + 活动流(2-3 天)
```
1. AiInputBar 组件(意图识别复用 Agent 逻辑)
2. QuickTasks 快捷按钮
3. ActivityFeed 活动流
4. 整合 NewHome.vue
```
涉及: NewHome.vue, AiInputBar.vue, ActivityFeed.vue, CoreStats.vue, QuickTasks.vue
### 阶段三:页面 AI 嵌入(3-4 天)
```
1. Translate.vue 增强
2. Customers.vue AI 操作
3. Products/Quotations AI 联动
4. Analytics.vue AI 摘要
```
### 阶段四:功能合并收尾(2-3 天)
```
1. AiAssistant → Agent 对话模式
2. 保留隐藏路由但移除导航入口
3. 全局空状态引导
4. 移动端适配
```
### 工时汇总
| 阶段 | 工时 | 可单独上线 |
|------|------|-----------|
| 一:导航精简 | 2-3 天 | ✅ 是 |
| 二:AI 输入栏 | 2-3 天 | ✅ 是 |
| 三:页面 AI 嵌入 | 3-4 天 | ❌ 建议与前序合并 |
| 四:功能合并 | 2-3 天 | ✅ 是 |
| **合计** | **9-13 天** | |
---
## 附录:参考案例
| 产品 | 参考点 |
|------|--------|
| **ChatGPT** | 统一输入 → 意图识别 → 结果展示 |
| **Perplexity** | 输入即搜索,结果结构化 |
| **Cursor** | AI 嵌入编辑器,inline 操作 |
| **Notion AI** | 自然语言 + 结构化输出 |
| **外贸 SaaS 案例** | AI 出现在需要时刻,不单独跳转 |
+144 -2
View File
@@ -6,13 +6,69 @@
<el-dialog
v-model="visible"
title="TradeMate AI 助手"
width="480px"
:close-on-click-modal="false"
class="ai-dialog"
top="5vh"
@opened="onOpened"
>
<template #header>
<div class="ai-header">
<span>{{ agentMode ? '数字员工' : 'TradeMate AI 助手' }}</span>
<el-switch
v-model="agentMode"
active-text="数字员工"
inactive-text="AI助手"
size="small"
style="margin-left:12px"
/>
</div>
</template>
<!-- Agent Mode: Pipeline form -->
<div v-if="agentMode" class="ai-agent-panel">
<div class="ai-agent-intro">
输入产品信息AI 数字员工将自动搜索客户分析匹配并生成开发信
</div>
<el-form label-position="top" style="padding:0 4px">
<el-form-item label="产品名称">
<el-input v-model="agentForm.product_name" placeholder="如:LED路灯" />
</el-form-item>
<el-form-item label="产品描述">
<el-input v-model="agentForm.product_description" type="textarea" :rows="3" placeholder="描述产品的规格、优势、用途..." />
</el-form-item>
<el-form-item label="目标市场(可选)">
<el-input v-model="agentForm.target_market" placeholder="如:美国、德国,留空为全球" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="agentRunning" @click="startAgent" style="width:100%">
{{ agentRunning ? '运行中...' : '🚀 启动数字员工' }}
</el-button>
</el-form-item>
</el-form>
<div v-if="agentPipeline" class="ai-agent-result">
<el-divider />
<div class="ai-agent-stages">
<div v-for="(stage, si) in agentPipeline.pipeline_data?.stages || []" :key="si" class="ai-agent-stage">
<el-steps :active="stage.completed ? 1 : (stage.running ? 0 : -1)" size="small" align-center>
<el-step :title="stage.name" :status="stage.completed ? 'success' : (stage.running ? 'process' : 'wait')" />
</el-steps>
<div v-if="stage.error" class="ai-stage-error">{{ stage.error }}</div>
</div>
</div>
<div v-if="agentPipeline.pipeline_data?.leads?.length" class="ai-agent-leads">
<div class="ai-agent-leads-title">发现 {{ agentPipeline.pipeline_data.leads.length }} 个高匹配客户</div>
<div v-for="(lead, li) in agentPipeline.pipeline_data.leads.slice(0, 5)" :key="li" class="ai-agent-lead">
<strong>{{ lead.company || lead.name || '未知' }}</strong>
<span v-if="lead.match_score" class="ai-match-badge">匹配度 {{ lead.match_score }}%</span>
</div>
</div>
</div>
</div>
<!-- Chat Mode -->
<template v-if="!agentMode">
<div class="ai-messages" ref="msgContainer">
<div v-for="(msg, i) in messages" :key="i" class="ai-msg-row" :class="msg.role">
<div class="ai-avatar" v-if="msg.role === 'assistant'">
@@ -59,9 +115,10 @@
</div>
</div>
</div>
</template>
<template #footer>
<div class="ai-input-bar">
<div v-if="!agentMode" class="ai-input-bar">
<el-input
v-model="inputText"
placeholder="输入你的问题..."
@@ -84,6 +141,7 @@ import {
aiChat, aiQuickQuestions,
createCustomer, createProduct, createQuotation,
scanFollowups, generateMarketing, discoverySearch,
startAgentPipeline,
} from '@/api'
const visible = ref(false)
@@ -96,6 +154,11 @@ const messages = ref([
])
const showSuggestions = ref(false)
const agentMode = ref(false)
const agentForm = ref({ product_name: '', product_description: '', target_market: '' })
const agentRunning = ref(false)
const agentPipeline = ref(null)
const fieldLabel = (type, key) => {
const labels = {
name: '名称', phone: '电话', email: '邮箱', company: '公司',
@@ -239,6 +302,28 @@ const scrollToBottom = async () => {
const el = msgContainer.value
if (el) el.scrollTop = el.scrollHeight
}
async function startAgent() {
if (!agentForm.value.product_name?.trim()) {
ElMessage.warning('产品名称不能为空')
return
}
agentRunning.value = true
agentPipeline.value = null
try {
const res = await startAgentPipeline({
product_name: agentForm.value.product_name.trim(),
product_description: agentForm.value.product_description?.trim() || '',
target_market: agentForm.value.target_market?.trim() || '',
})
agentPipeline.value = res.data || res
ElMessage.success('数字员工任务完成!')
} catch (e) {
ElMessage.error(e?.detail || e?.message || '启动数字员工失败')
} finally {
agentRunning.value = false
}
}
</script>
<style scoped>
@@ -379,4 +464,61 @@ const scrollToBottom = async () => {
padding: 10px 16px;
border-top: 1px solid #f0f0f0;
}
.ai-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.ai-agent-panel {
padding: 20px 16px;
overflow-y: auto;
height: 100%;
}
.ai-agent-intro {
font-size: 13px;
color: #666;
line-height: 1.6;
margin-bottom: 16px;
padding: 12px;
background: #f0f5ff;
border-radius: 6px;
}
.ai-agent-result {
margin-top: 8px;
}
.ai-agent-stages {
margin-bottom: 12px;
}
.ai-agent-stage {
margin-bottom: 8px;
}
.ai-stage-error {
color: #f56c6c;
font-size: 12px;
margin-top: 4px;
}
.ai-agent-leads-title {
font-weight: 600;
font-size: 14px;
margin-bottom: 8px;
color: #303133;
}
.ai-agent-lead {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
margin-bottom: 6px;
background: #f9f9f9;
border-radius: 6px;
font-size: 13px;
}
.ai-match-badge {
background: #e6f7e6;
color: #52c41a;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
</style>
+26 -17
View File
@@ -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
View File
@@ -1,5 +1,8 @@
{
"nav": {
"home": "Workspace",
"more": "More",
"productsQuotations": "Products & Quotes",
"discovery": "Find Buyers",
"workspace": "Dashboard",
"customers": "Customers",
+3
View File
@@ -1,5 +1,8 @@
{
"nav": {
"home": "首页工作台",
"more": "更多",
"productsQuotations": "报价产品",
"discovery": "发现客户",
"workspace": "工作台",
"customers": "客户管理",
+32 -107
View File
@@ -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: '/' },
]
+32
View File
@@ -1,5 +1,9 @@
<template>
<div v-loading="loading" element-loading-text="加载数据分析中...">
<transition name="el-fade-in">
<el-alert v-if="aiSummary" :title="aiSummary" type="success" :closable="false" show-icon style="margin-bottom:20px" />
</transition>
<el-row :gutter="20">
<el-col :xs="12" :sm="6" v-for="item in cards" :key="item.label">
<el-card shadow="hover" style="text-align:center;margin-bottom:20px">
@@ -48,6 +52,33 @@ const loading = ref(true)
const cards = ref([])
const statusData = ref([])
const countryData = ref([])
const aiSummary = ref('')
function generateAiSummary(d) {
const customers = d.customers || {}
const total = customers.total || 0
const statusCounts = customers.status_counts || customers.by_status || {}
const leads = statusCounts.lead || 0
const negotiating = statusCounts.negotiating || 0
const deals = statusCounts.customer || 0
const countries = customers.by_country || d.countries || []
const topCountry = countries.length ? (countries[0].country || countries[0].name) : '—'
const todayTrans = d.translations?.today || 0
const todayMsgs = d.messages?.today || 0
const parts = []
if (total > 0) {
parts.push(`共有 ${total} 个客户`)
if (leads > 0) parts.push(`${leads} 个潜在客户待开发`)
if (negotiating > 0) parts.push(`${negotiating} 个正在洽谈中`)
if (deals > 0) parts.push(`已成交 ${deals}`)
}
if (topCountry !== '—') parts.push(`主要市场集中在 ${topCountry}`)
if (todayTrans > 0) parts.push(`今日翻译 ${todayTrans}`)
if (todayMsgs > 0) parts.push(`今日消息 ${todayMsgs}`)
return parts.length > 0 ? '📊 ' + parts.join('') + '。' : ''
}
onMounted(async () => {
loading.value = true
@@ -70,6 +101,7 @@ onMounted(async () => {
const countries = customers.by_country || d.countries || []
const countryTotal = countries.reduce((a, b) => a + (b.count || b.value || 0), 0) || 1
countryData.value = (countries.slice ? countries.slice(0, 10) : []).map(c => ({ country: c.country || c.name, count: c.count || c.value || 0, pct: Math.round((c.count || c.value || 0) / countryTotal * 100) }))
aiSummary.value = generateAiSummary(d)
} catch { ElMessage.error('加载分析数据失败') }
finally { loading.value = false }
})
+24
View File
@@ -26,6 +26,13 @@
<el-tag :type="healthType(row.health_grade)" size="small">{{ row.health_grade || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="AI建议" width="130">
<template #default="{ row }">
<el-tag :type="aiSuggestionType(row)" size="small" effect="plain" style="cursor:default">
{{ aiSuggestion(row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="viewDetail(row)">详情</el-button>
@@ -109,6 +116,23 @@ function statusType(s) { return { lead: 'info', negotiating: 'warning', customer
function statusLabel(s) { return { lead: '潜在客户', negotiating: '洽谈中', customer: '已成交', lost: '流失' }[s] || s }
function healthType(g) { return { A: 'success', B: 'warning', C: 'danger' }[g] || 'info' }
function aiSuggestion(row) {
if (row.status === 'lost') return '流失客户'
if (row.status === 'customer' && row.health_grade === 'C') return '需要挽回'
if (row.status === 'customer') return '维护关系'
if (row.status === 'negotiating') return '尽快报价'
if (!row.last_contact || Date.now() - new Date(row.last_contact).getTime() > 30 * 24 * 60 * 60 * 1000) return '建议跟进'
return '可跟进'
}
function aiSuggestionType(row) {
if (row.status === 'lost') return 'danger'
if (row.status === 'customer' && row.health_grade === 'C') return 'danger'
if (row.status === 'customer') return 'success'
if (row.status === 'negotiating') return 'warning'
if (!row.last_contact || Date.now() - new Date(row.last_contact).getTime() > 30 * 24 * 60 * 60 * 1000) return 'warning'
return 'info'
}
onMounted(load)
async function load() {
+679
View File
@@ -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>
+10 -5
View File
@@ -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>
+22 -3
View File
@@ -10,14 +10,18 @@
<el-option label="西班牙语" value="es" />
<el-option label="日语" value="ja" />
</el-select>
<el-switch v-model="autoDetect" active-text="自动检测" style="margin-right:4px" />
<el-button type="primary" :loading="loading" @click="doTranslate">翻译</el-button>
<el-button @click="showReply = !showReply">回复建议</el-button>
<el-button @click="showExtract = !showExtract">信息提取</el-button>
</div>
<div v-if="result" style="padding:16px;background:#f5f5f5;border-radius:6px">
<p style="white-space:pre-wrap">{{ result }}</p>
<div style="margin-top:8px;display:flex;gap:8px">
<div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap">
<el-button text type="primary" size="small" @click="copyText(result)">复制</el-button>
<el-button text type="success" size="small" @click="quickReply">
<el-icon style="margin-right:4px"><ChatLineSquare /></el-icon>一键回复
</el-button>
</div>
</div>
</el-card>
@@ -51,11 +55,13 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, watch } from 'vue'
import { translate, translateReply, extractInfo } from '@/api'
import { ElMessage } from 'element-plus'
import { ChatLineSquare } from '@element-plus/icons-vue'
const form = ref({ text: '', target_lang: 'en' })
const autoDetect = ref(true)
const loading = ref(false)
const result = ref('')
const showReply = ref(false)
@@ -67,12 +73,18 @@ const extractText = ref('')
const extractLoading = ref(false)
const extractResult = ref('')
watch(autoDetect, (v) => {
if (v) form.value.target_lang = 'en'
})
async function doTranslate() {
if (!form.value.text.trim()) return
loading.value = true
result.value = '翻译中...'
try {
const res = await translate(form.value)
const payload = { text: form.value.text, target_lang: form.value.target_lang }
if (autoDetect.value) payload.source_lang = 'auto'
const res = await translate(payload)
result.value = res.data?.translated_text || res.translated_text || ''
} catch {
result.value = ''
@@ -81,6 +93,13 @@ async function doTranslate() {
finally { loading.value = false }
}
function quickReply() {
if (!result.value) return
replyInquiry.value = form.value.text
showReply.value = true
getReply()
}
async function getReply() {
if (!replyInquiry.value.trim()) return
replyLoading.value = true