Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ccaafe470 | |||
| 417e79c64d | |||
| cb8faed80f | |||
| 771b70843f | |||
| b1ffb78629 |
@@ -1,5 +1,17 @@
|
||||
# TradeMate (外贸小助手) — Agent Guide
|
||||
|
||||
## uni-app 移动端 H5 (改版后)
|
||||
|
||||
- **Tab Bar**: 4 项底部导航 — 首页 / 客户 / 业务 / 我的
|
||||
- **主页** `pages/index/index.vue` — 简化仪表盘 (统计卡片 + 快捷入口矩阵 + 待办提醒)
|
||||
- **客户工作台** `pages/workspace-customer/workspace-customer.vue` — **NEW** 3 个内联标签 (客户列表 / 挖掘新客 / 智能跟进)
|
||||
- **业务工作台** `pages/workspace-biz/workspace-biz.vue` — **NEW** 4 个内联标签 (翻译 / 产品库 / 报价单 / 营销素材)
|
||||
- **个人中心** `pages/profile/profile.vue` — 更新导航 (通知中心 / 数据分析 / 团队 / 升级会员 / 联系客服)
|
||||
- **旧页面保留** (customers, discovery, followup, translate, product, quotation, marketing 等仍可通过 deep link 访问)
|
||||
- **pages.json** — 标签栏从 5 项变为 4 项,新增 workspace-customer 和 workspace-biz 页面
|
||||
- **config.js** — PAGES 配置新增 WORKSPACE_CUSTOMER / WORKSPACE_BIZ
|
||||
- **部署路径**: `trade.yuzhiran.com/app/` — nginx SPA fallback `try_files $uri $uri/ /app/index.html`
|
||||
|
||||
## AI Assistant (Frontend AI Chatbot)
|
||||
|
||||
- **Components**: `user-frontend/src/components/AiAssistant.vue` + `admin-frontend/src/components/AiAssistant.vue`
|
||||
|
||||
@@ -57,7 +57,7 @@ const configs = ref([])
|
||||
const edits = reactive({})
|
||||
const providers = ref([])
|
||||
const configLabels = { system_maintenance: '系统维护', feature_flags: '功能开关', translation_providers: '翻译服务', ai_model_config: 'AI模型', cache_ttl: '缓存时间', ai_routing: 'AI路由规则' }
|
||||
const fieldLabelsMap = { system_maintenance: { maintenance_mode: '维护模式', maintenance_message: '维护消息' }, feature_flags: { feature_wechat_login: '微信登录', feature_export: '数据导出' }, translation_providers: { primary: '首选服务', fallback: '备用服务' }, ai_model_config: { default_model: '默认模型', max_tokens: '最大Token' }, ai_routing: { translate: '翻译', reply: '回复建议', marketing: '营销文案', extract: '信息提取', quotation: '报价单', chat: 'AI助手' } }
|
||||
const fieldLabelsMap = { system_maintenance: { maintenance_mode: '维护模式', maintenance_message: '维护消息' }, feature_flags: { feature_wechat_login: '微信登录', feature_export: '数据导出' }, translation_providers: { primary: '首选服务', fallback: '备用服务' }, ai_model_config: { default_model: '默认模型', max_tokens: '最大Token' }, ai_routing: { translate: '翻译', reply: '回复建议', marketing: '营销文案', extract: '信息提取', quotation: '报价单', chat: 'AI助手', agent: '数字员工', followup: '智能跟进', outreach: '开发信', competitor: '竞品分析' } }
|
||||
const taskFieldLabels = { primary: '主选', fallback: '备用' }
|
||||
function fieldLabel(key, k) {
|
||||
if (key === 'ai_routing') return configLabels[key] + ' > ' + (taskFieldLabels[k] || k)
|
||||
|
||||
@@ -10,11 +10,13 @@ export const STORAGE_KEYS = {
|
||||
|
||||
export const PAGES = {
|
||||
INDEX: '/pages/index/index',
|
||||
WORKSPACE_CUSTOMER: '/pages/workspace-customer/workspace-customer',
|
||||
WORKSPACE_BIZ: '/pages/workspace-biz/workspace-biz',
|
||||
PROFILE: '/pages/profile/profile',
|
||||
TRANSLATE: '/pages/translate/translate',
|
||||
CUSTOMERS: '/pages/customers/customers',
|
||||
MARKETING: '/pages/marketing/marketing',
|
||||
QUOTATION: '/pages/quotation/quotation',
|
||||
PROFILE: '/pages/profile/profile',
|
||||
LOGIN: '/pages/login/login',
|
||||
PRODUCT: '/pages/product/product',
|
||||
UPGRADE: '/pages/upgrade/upgrade',
|
||||
|
||||
+17
-7
@@ -18,6 +18,20 @@
|
||||
"navigationBarTitleText": "客户管理"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/workspace-customer/workspace-customer",
|
||||
"style": {
|
||||
"navigationBarTitleText": "客户工作台",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/workspace-biz/workspace-biz",
|
||||
"style": {
|
||||
"navigationBarTitleText": "业务工作台",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/marketing/marketing",
|
||||
"style": {
|
||||
@@ -145,16 +159,12 @@
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/customers/customers",
|
||||
"pagePath": "pages/workspace-customer/workspace-customer",
|
||||
"text": "客户"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/marketing/marketing",
|
||||
"text": "营销"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/quotation/quotation",
|
||||
"text": "报价"
|
||||
"pagePath": "pages/workspace-biz/workspace-biz",
|
||||
"text": "业务"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/profile",
|
||||
|
||||
@@ -130,25 +130,33 @@
|
||||
</view>
|
||||
|
||||
<view class="more-section">
|
||||
<view class="section-title">功能矩阵</view>
|
||||
<view class="section-title">快捷入口</view>
|
||||
<view class="more-grid">
|
||||
<view class="more-item" @click="goToPage(PAGES.TRANSLATE)">
|
||||
<view class="more-item" @click="goToPage(PAGES.WORKSPACE_BIZ)">
|
||||
<text class="more-icon">🔤</text>
|
||||
<text class="more-text">翻译</text>
|
||||
</view>
|
||||
<view class="more-item" @click="goToPage(PAGES.DISCOVERY)">
|
||||
<view class="more-item" @click="goToPage(PAGES.WORKSPACE_CUSTOMER)">
|
||||
<text class="more-icon">🔍</text>
|
||||
<text class="more-text">挖掘新客</text>
|
||||
<text class="more-text">客户</text>
|
||||
</view>
|
||||
<view class="more-item" @click="hasLogin ? goToPage(PAGES.PRODUCT) : goToLogin()">
|
||||
<view class="more-item" @click="hasLogin ? goToPage(PAGES.WORKSPACE_BIZ) : goToLogin()">
|
||||
<text class="more-icon">📦</text>
|
||||
<text class="more-text">产品库</text>
|
||||
<text class="more-text">产品</text>
|
||||
</view>
|
||||
<view class="more-item" @click="hasLogin ? goToPage(PAGES.FOLLOWUP) : goToLogin()">
|
||||
<view class="more-item" @click="hasLogin ? goToPage(PAGES.WORKSPACE_CUSTOMER) : goToLogin()">
|
||||
<text class="more-icon">📋</text>
|
||||
<text class="more-text">跟进</text>
|
||||
<text class="notif-badge" v-if="hasLogin && followupStats.pending > 0">{{ followupStats.pending > 99 ? '99+' : followupStats.pending }}</text>
|
||||
</view>
|
||||
<view class="more-item" @click="hasLogin ? goToPage(PAGES.WORKSPACE_BIZ) : goToLogin()">
|
||||
<text class="more-icon">💰</text>
|
||||
<text class="more-text">报价</text>
|
||||
</view>
|
||||
<view class="more-item" @click="hasLogin ? goToPage(PAGES.WORKSPACE_BIZ) : goToLogin()">
|
||||
<text class="more-icon">📝</text>
|
||||
<text class="more-text">营销</text>
|
||||
</view>
|
||||
<view class="more-item" @click="hasLogin ? goToPage(PAGES.NOTIFICATION) : goToLogin()">
|
||||
<text class="more-icon">🔔</text>
|
||||
<text class="more-text">通知</text>
|
||||
|
||||
@@ -19,6 +19,26 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">工作台</view>
|
||||
<view class="menu-item" @click="goNotification">
|
||||
<text class="menu-icon">🔔</text>
|
||||
<text class="menu-text">通知中心</text>
|
||||
<text class="badge-count" v-if="unreadCount > 0">{{ unreadCount > 99 ? '99+' : unreadCount }}</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goAnalytics">
|
||||
<text class="menu-icon">📊</text>
|
||||
<text class="menu-text">数据分析</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goTeam">
|
||||
<text class="menu-icon">👥</text>
|
||||
<text class="menu-text">团队协作</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">账号设置</view>
|
||||
<view class="menu-item" v-if="user.tier !== 'guest'" @click="showProfileEdit = true">
|
||||
@@ -59,6 +79,16 @@
|
||||
<text class="menu-text">意见反馈</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goPage('/pages/upgrade/upgrade')">
|
||||
<text class="menu-icon">💎</text>
|
||||
<text class="menu-text">升级会员</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="showWechatModal = true">
|
||||
<text class="menu-icon">💁</text>
|
||||
<text class="menu-text">联系客服</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
@@ -138,12 +168,13 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { authApi, creditApi } from '@/utils/api.js'
|
||||
import { authApi, creditApi, notificationApi } from '@/utils/api.js'
|
||||
import AiAssistant from '@/components/ai-assistant.vue'
|
||||
import { STORAGE_KEYS, PAGES, TIER_LABELS } from '@/config.js'
|
||||
|
||||
const user = ref({})
|
||||
const creditBalance = ref(0)
|
||||
const unreadCount = ref(0)
|
||||
const showProfileEdit = ref(false)
|
||||
const showPassword = ref(false)
|
||||
const editForm = ref({ username: '', email: '' })
|
||||
@@ -217,6 +248,17 @@ const goFeedback = () => uni.navigateTo({ url: PAGES.FEEDBACK })
|
||||
const goAgreement = (type) => uni.navigateTo({ url: `/pages/agreement/${type}` })
|
||||
const goCertification = () => uni.navigateTo({ url: PAGES.CERTIFICATION })
|
||||
const goInvoice = () => uni.navigateTo({ url: PAGES.INVOICE })
|
||||
const goNotification = () => uni.navigateTo({ url: PAGES.NOTIFICATION })
|
||||
const goAnalytics = () => uni.navigateTo({ url: PAGES.ANALYTICS })
|
||||
const goTeam = () => uni.navigateTo({ url: PAGES.TEAM })
|
||||
const goPage = (url) => uni.navigateTo({ url })
|
||||
|
||||
const loadUnread = async () => {
|
||||
try {
|
||||
const res = await notificationApi.unreadCount()
|
||||
unreadCount.value = res.count || 0
|
||||
} catch { unreadCount.value = 0 }
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
uni.showModal({
|
||||
@@ -232,8 +274,8 @@ const logout = () => {
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(loadUser)
|
||||
onShow(loadUser)
|
||||
onMounted(() => { loadUser(); loadUnread() })
|
||||
onShow(() => { loadUser(); loadUnread() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,335 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="visible" class="commandk-overlay" @click.self="close" />
|
||||
<div v-if="visible" class="commandk-palette" @keydown.esc="close">
|
||||
<div class="commandk-input-wrap">
|
||||
<el-icon class="commandk-search-icon"><Search /></el-icon>
|
||||
<input
|
||||
ref="inputRef"
|
||||
v-model="query"
|
||||
class="commandk-input"
|
||||
:placeholder="$t('nav.home') === 'Home' ? 'Search actions or type a question...' : '搜索功能或输入问题...'"
|
||||
@keydown.enter.prevent="handleEnter"
|
||||
@keydown.down.prevent="moveSelection(1)"
|
||||
@keydown.up.prevent="moveSelection(-1)"
|
||||
/>
|
||||
<span class="commandk-hint">⌘K</span>
|
||||
</div>
|
||||
|
||||
<div v-if="aiMode" class="commandk-ai-section">
|
||||
<div class="commandk-ai-msgs" ref="aiMsgRef">
|
||||
<div v-for="(msg, i) in aiMessages" :key="i" class="commandk-ai-msg" :class="msg.role">
|
||||
<div class="commandk-ai-bubble">{{ msg.content }}</div>
|
||||
</div>
|
||||
<div v-if="aiLoading" class="commandk-ai-msg assistant">
|
||||
<div class="commandk-ai-bubble commandk-ai-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>思考中...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="commandk-ai-input-row">
|
||||
<el-input
|
||||
v-model="aiInput"
|
||||
:placeholder="$t('nav.home') === 'Home' ? 'Ask AI anything...' : '向 AI 提问...'"
|
||||
size="small"
|
||||
@keyup.enter="sendAiMessage"
|
||||
/>
|
||||
<el-button type="primary" size="small" :disabled="!aiInput.trim() || aiLoading" @click="sendAiMessage">
|
||||
<el-icon><Promotion /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredActions.length" class="commandk-results">
|
||||
<div
|
||||
v-for="(action, i) in filteredActions"
|
||||
:key="action.id"
|
||||
class="commandk-item"
|
||||
:class="{ selected: i === selectedIndex }"
|
||||
@click="executeAction(action)"
|
||||
@mouseenter="selectedIndex = i"
|
||||
>
|
||||
<el-icon :size="16"><component :is="action.icon" /></el-icon>
|
||||
<div class="commandk-item-content">
|
||||
<div class="commandk-item-title">{{ action.label }}</div>
|
||||
<div v-if="action.desc" class="commandk-item-desc">{{ action.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="query" class="commandk-empty">
|
||||
<p>{{ $t('common.no_data') }}</p>
|
||||
<el-button text type="primary" @click="switchToAi">{{ $t('nav.home') === 'Home' ? 'Ask AI directly' : '直接向 AI 提问' }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Search, Promotion, Loading } from '@element-plus/icons-vue'
|
||||
import { aiChat } from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const visible = ref(false)
|
||||
const query = ref('')
|
||||
const selectedIndex = ref(0)
|
||||
const inputRef = ref(null)
|
||||
const aiMode = ref(false)
|
||||
const aiInput = ref('')
|
||||
const aiMessages = ref([])
|
||||
const aiLoading = ref(false)
|
||||
const aiMsgRef = ref(null)
|
||||
|
||||
const actions = computed(() => [
|
||||
{ id: 'home', label: t('nav.home'), desc: t('nav.home') === 'Home' ? 'Back to dashboard' : '返回首页', icon: 'Odometer', route: '/workspace' },
|
||||
{ id: 'customers', label: t('nav.customers'), desc: t('nav.home') === 'Home' ? 'Manage customers, discover leads, follow-ups' : '客户管理、发现客户、跟进提醒', icon: 'User', route: '/workspace/customers' },
|
||||
{ id: 'biz', label: t('nav.biz'), desc: t('nav.home') === 'Home' ? 'Translate, quotations, products, marketing' : '翻译、报价、产品、营销', icon: 'Goods', route: '/workspace/biz' },
|
||||
{ id: 'analytics', label: t('nav.analytics'), desc: t('nav.home') === 'Home' ? 'Data analysis & reports' : '数据分析和报表', icon: 'DataAnalysis', route: '/workspace/analytics' },
|
||||
{ id: 'profile', label: t('nav.profile'), desc: t('nav.home') === 'Home' ? 'Personal settings & profile' : '个人中心和设置', icon: 'User', route: '/workspace/profile' },
|
||||
{ id: 'team', label: t('nav.team'), desc: t('nav.home') === 'Home' ? 'Team collaboration' : '团队协作', icon: 'UserFilled', route: '/workspace/team' },
|
||||
{ id: 'credits', label: t('nav.credits'), desc: t('nav.home') === 'Home' ? 'Purchase credits & subscriptions' : '购买次数和套餐', icon: 'Coin', route: '/workspace/profile/credits' },
|
||||
{ id: 'notifications', label: t('nav.notifications'), desc: t('nav.home') === 'Home' ? 'View notifications' : '查看通知', icon: 'Bell', route: '/workspace/profile/notifications' },
|
||||
{ id: 'translate', label: t('nav.translate'), desc: 'AI 智能翻译', icon: 'ChatLineSquare', route: '/workspace/biz' },
|
||||
{ id: 'products', label: t('nav.products'), desc: '管理产品库', icon: 'Goods', route: '/workspace/biz' },
|
||||
{ id: 'quotations', label: t('nav.quotations'), desc: '管理报价单', icon: 'DocumentCopy', route: '/workspace/biz' },
|
||||
{ id: 'discovery', label: t('nav.discovery'), desc: '搜索潜在买家', icon: 'Search', route: '/workspace/customers' },
|
||||
{ id: 'followup', label: t('nav.followup'), desc: '跟进提醒', icon: 'AlarmClock', route: '/workspace/customers' },
|
||||
{ id: 'marketing', label: t('nav.marketing'), desc: '生成营销素材', icon: 'EditPen', route: '/workspace/biz' },
|
||||
])
|
||||
|
||||
const filteredActions = computed(() => {
|
||||
const q = query.value.toLowerCase().trim()
|
||||
if (!q) return actions.value
|
||||
return actions.value.filter(a =>
|
||||
a.label.toLowerCase().includes(q) ||
|
||||
(a.desc && a.desc.toLowerCase().includes(q))
|
||||
)
|
||||
})
|
||||
|
||||
watch(visible, (v) => {
|
||||
if (v) {
|
||||
query.value = ''
|
||||
selectedIndex.value = 0
|
||||
aiMode.value = false
|
||||
aiMessages.value = []
|
||||
aiInput.value = ''
|
||||
nextTick(() => inputRef.value?.focus())
|
||||
}
|
||||
})
|
||||
|
||||
watch(query, () => { selectedIndex.value = 0 })
|
||||
|
||||
function moveSelection(delta) {
|
||||
const len = filteredActions.value.length
|
||||
if (!len) return
|
||||
selectedIndex.value = (selectedIndex.value + delta + len) % len
|
||||
}
|
||||
|
||||
function switchToAi() {
|
||||
aiMode.value = true
|
||||
nextTick(() => {
|
||||
const el = document.querySelector('.commandk-ai-input-row input')
|
||||
el?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
function executeAction(action) {
|
||||
close()
|
||||
router.push(action.route)
|
||||
}
|
||||
|
||||
async function sendAiMessage() {
|
||||
const msg = aiInput.value.trim()
|
||||
if (!msg || aiLoading.value) return
|
||||
aiInput.value = ''
|
||||
aiMessages.value.push({ role: 'user', content: msg })
|
||||
aiLoading.value = true
|
||||
try {
|
||||
const hist = aiMessages.value.map(m => ({ role: m.role, content: m.content }))
|
||||
const res = await aiChat(msg, hist.slice(0, -1))
|
||||
aiMessages.value.push({ role: 'assistant', content: res.reply || '抱歉,我没有理解。' })
|
||||
} catch (e) {
|
||||
console.error('[CommandK] AI chat error:', e)
|
||||
const detail = e?.detail || e?.message || ''
|
||||
aiMessages.value.push({ role: 'assistant', content: detail ? `请求失败:${detail}` : '请求失败,请稍后重试。' })
|
||||
} finally {
|
||||
aiLoading.value = false
|
||||
nextTick(() => {
|
||||
if (aiMsgRef.value) aiMsgRef.value.scrollTop = aiMsgRef.value.scrollHeight
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handleEnter() {
|
||||
if (aiMode.value) { sendAiMessage(); return }
|
||||
const items = filteredActions.value
|
||||
if (items[selectedIndex.value]) executeAction(items[selectedIndex.value])
|
||||
}
|
||||
|
||||
function close() { visible.value = false }
|
||||
|
||||
function handleKeydown(e) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault()
|
||||
visible.value = !visible.value
|
||||
}
|
||||
if (e.key === 'Escape' && visible.value) {
|
||||
if (aiMode.value) { aiMode.value = false; return }
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('keydown', handleKeydown))
|
||||
onUnmounted(() => document.removeEventListener('keydown', handleKeydown))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.commandk-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.35);
|
||||
z-index: 9998;
|
||||
}
|
||||
.commandk-palette {
|
||||
position: fixed;
|
||||
top: 12vh;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 560px;
|
||||
max-width: calc(100vw - 32px);
|
||||
max-height: 70vh;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 16px 48px rgba(0,0,0,0.2);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.commandk-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
gap: 10px;
|
||||
}
|
||||
.commandk-search-icon {
|
||||
color: #999;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.commandk-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 15px;
|
||||
background: transparent;
|
||||
color: #333;
|
||||
}
|
||||
.commandk-hint {
|
||||
font-size: 11px;
|
||||
color: #bbb;
|
||||
background: #f5f5f5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.commandk-results {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 6px 0;
|
||||
}
|
||||
.commandk-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.commandk-item:hover,
|
||||
.commandk-item.selected {
|
||||
background: #f0f5ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
.commandk-item-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.commandk-item-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.commandk-item-desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.commandk-empty {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
.commandk-ai-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.commandk-ai-msgs {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.commandk-ai-msg {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.commandk-ai-msg.user {
|
||||
text-align: right;
|
||||
}
|
||||
.commandk-ai-bubble {
|
||||
display: inline-block;
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
max-width: 85%;
|
||||
background: #f0f5ff;
|
||||
color: #333;
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.commandk-ai-msg.user .commandk-ai-bubble {
|
||||
background: #667eea;
|
||||
color: #fff;
|
||||
}
|
||||
.commandk-ai-loading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: #f5f5f5 !important;
|
||||
color: #999 !important;
|
||||
}
|
||||
.commandk-ai-input-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.commandk-palette { top: 8vh; max-height: 80vh; width: calc(100vw - 16px); border-radius: 8px; }
|
||||
.commandk-item { padding: 8px 12px; }
|
||||
.commandk-item-desc { font-size: 11px; }
|
||||
.commandk-ai-bubble { max-width: 90%; font-size: 12px; }
|
||||
}
|
||||
</style>
|
||||
@@ -5,10 +5,15 @@
|
||||
<aside class="sidebar" :class="{ collapsed, 'mobile-show': showMobileMenu }">
|
||||
<div class="sidebar-header">
|
||||
<router-link to="/workspace" class="logo">{{ collapsed ? 'TM' : 'TradeMate' }}</router-link>
|
||||
<el-button v-if="showMobileMenu" text style="color:#999;font-size:20px;margin-left:auto" @click="showMobileMenu = false">
|
||||
<div class="sidebar-header-actions">
|
||||
<el-button text class="collapse-btn dt-only" @click="collapsed = !collapsed">
|
||||
<el-icon><Fold v-if="!collapsed" /><Expand v-else /></el-icon>
|
||||
</el-button>
|
||||
<el-button v-if="showMobileMenu" text class="collapse-btn" @click="showMobileMenu = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="route.path"
|
||||
:collapse="collapsed"
|
||||
@@ -16,24 +21,16 @@
|
||||
:collapse-transition="false"
|
||||
@select="showMobileMenu = false"
|
||||
>
|
||||
<el-menu-item index="/workspace"><el-icon><Odometer /></el-icon><span>{{ $t('nav.home') || '首页工作台' }}</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-menu-item index="/workspace/biz"><el-icon><Goods /></el-icon><span>{{ $t('nav.biz') }}</span></el-menu-item>
|
||||
<el-menu-item index="/workspace/analytics"><el-icon><DataAnalysis /></el-icon><span>{{ $t('nav.analytics') }}</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>
|
||||
@@ -62,7 +59,7 @@
|
||||
<el-dropdown trigger="click">
|
||||
<el-button text style="display:flex;align-items:center;gap:6px">
|
||||
<el-icon><User /></el-icon>
|
||||
<span class="user-name">{{ auth.user?.username || '用户' }}</span>
|
||||
<span class="user-name">{{ displayName }}</span>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
@@ -80,41 +77,44 @@
|
||||
<router-view />
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="footer-content">
|
||||
<p>© {{ new Date().getFullYear() }} TradeMate</p>
|
||||
<div class="footer-links">
|
||||
<a href="http://beian.miit.gov.cn/" target="_blank">{{ beianInfo.icp }}</a>
|
||||
<a v-if="beianInfo.showGongan" :href="beianInfo.gonganLink" target="_blank" rel="noreferrer" class="gongan-link">
|
||||
<img src="/images/beian/gongan-beian.png" alt="公安备案" class="gongan-icon" />
|
||||
{{ beianInfo.gongan }}
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<AiAssistant />
|
||||
<CommandK />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { getUnreadCount, getCreditBalance } from '@/api'
|
||||
import AiAssistant from '@/components/AiAssistant.vue'
|
||||
import CommandK from '@/components/CommandK.vue'
|
||||
import { switchLang } from '@/i18n'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { locale } = useI18n()
|
||||
const auth = useAuthStore()
|
||||
const collapsed = ref(false)
|
||||
const collapsed = ref(window.innerWidth < 1024)
|
||||
const showMobileMenu = ref(false)
|
||||
const unread = ref(0)
|
||||
const creditBalance = ref(null)
|
||||
|
||||
function handleResize() {
|
||||
const w = window.innerWidth
|
||||
collapsed.value = w < 1024
|
||||
if (w >= 768) showMobileMenu.value = false
|
||||
}
|
||||
onMounted(() => window.addEventListener('resize', handleResize))
|
||||
onUnmounted(() => window.removeEventListener('resize', handleResize))
|
||||
|
||||
const displayName = computed(() => {
|
||||
const u = auth.user
|
||||
if (u && typeof u === 'object' && typeof u.username === 'string') return u.username
|
||||
return '用户'
|
||||
})
|
||||
|
||||
const currentLang = computed(() => locale.value === 'en' ? 'English' : '中文')
|
||||
function toggleLang() {
|
||||
const next = locale.value === 'en' ? 'zh-CN' : 'en'
|
||||
@@ -136,17 +136,6 @@ onMounted(async () => {
|
||||
loadCreditBalance()
|
||||
})
|
||||
|
||||
const beianInfo = computed(() => {
|
||||
const hostname = window.location.hostname
|
||||
if (hostname === 'yuzhiran.com' || hostname === 'www.yuzhiran.com') {
|
||||
return { icp: '京ICP备2026007249号-1', gongan: '京公网安备11011502039545号', gonganLink: 'https://beian.mps.gov.cn/#/query/webSearch?code=11011502039545', showGongan: true }
|
||||
}
|
||||
if (hostname === 'yuzhiran.com.cn' || hostname === 'www.yuzhiran.com.cn') {
|
||||
return { icp: '京ICP备2026007249号-2', gongan: '京公网安备11011502039622号', gonganLink: 'https://beian.mps.gov.cn/#/query/webSearch?code=11011502039622', showGongan: true }
|
||||
}
|
||||
return { icp: '京ICP备2026007249号-1', gongan: '京公网安备11011502039545号', gonganLink: 'https://beian.mps.gov.cn/#/query/webSearch?code=11011502039545', showGongan: true }
|
||||
})
|
||||
|
||||
function handleLogout() {
|
||||
auth.logout()
|
||||
router.push('/')
|
||||
@@ -154,40 +143,90 @@ function handleLogout() {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ===== Base Layout ===== */
|
||||
.user-layout { display: flex; height: 100vh; overflow: hidden; }
|
||||
.sidebar-mask { display: none; }
|
||||
.sidebar { width: 220px; background: #fff; border-right: 1px solid #e8e8e8; transition: width 0.25s; flex-shrink: 0; display: flex; flex-direction: column; }
|
||||
.sidebar.collapsed { width: 64px; }
|
||||
.sidebar-header { height: 60px; display: flex; align-items: center; padding: 0 16px; color: #1890ff; font-size: 18px; font-weight: 700; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; }
|
||||
.sidebar :deep(.el-menu) { border-right: none; flex: 1; }
|
||||
.sidebar :deep(.el-menu-item) { margin: 2px 8px; border-radius: 8px; }
|
||||
.sidebar :deep(.el-menu-item.is-active) { background: #e6f7ff; color: #1890ff !important; font-weight: 500; }
|
||||
.main-area { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
.topbar { height: 60px; background: #fff; border-bottom: 1px solid #e8e8e8; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; gap: 12px; }
|
||||
.content { flex: 1; padding: 24px; overflow-y: auto; background: #f5f5f5; }
|
||||
|
||||
/* ===== Sidebar ===== */
|
||||
.sidebar {
|
||||
width: 220px; background: #fff; border-right: 1px solid #e8e8e8;
|
||||
transition: width 0.25s; flex-shrink: 0; display: flex; flex-direction: column;
|
||||
position: relative; z-index: 100;
|
||||
}
|
||||
.sidebar.collapsed { width: 64px; }
|
||||
.sidebar-header {
|
||||
height: 60px; display: flex; align-items: center; padding: 0 12px;
|
||||
color: #1890ff; font-size: 18px; font-weight: 700; border-bottom: 1px solid #f0f0f0; flex-shrink: 0;
|
||||
gap: 8px;
|
||||
}
|
||||
.sidebar-header .logo { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.sidebar-header-actions { display: flex; flex-shrink: 0; }
|
||||
.collapse-btn { font-size: 18px; color: #999; }
|
||||
.dt-only { display: none; }
|
||||
.sidebar :deep(.el-menu) { border-right: none; flex: 1; }
|
||||
.sidebar :deep(.el-menu-item) { margin: 2px 8px; border-radius: 8px; white-space: nowrap; }
|
||||
.sidebar :deep(.el-menu-item.is-active) { background: #e6f7ff; color: #1890ff !important; font-weight: 500; }
|
||||
|
||||
/* ===== Topbar ===== */
|
||||
.topbar {
|
||||
height: 60px; background: #fff; border-bottom: 1px solid #e8e8e8;
|
||||
display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; gap: 12px;
|
||||
}
|
||||
.menu-btn { display: none; font-size: 20px; }
|
||||
.breadcrumb { flex: 1; min-width: 0; }
|
||||
.breadcrumb { flex: 1; min-width: 0; overflow: hidden; }
|
||||
.breadcrumb :deep(.el-breadcrumb__inner) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 160px; display: inline-block; vertical-align: middle; }
|
||||
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
||||
.notif-badge :deep(.el-badge__content) { top: 8px; right: 4px; }
|
||||
.content { flex: 1; padding: 24px; overflow-y: auto; background: #f5f5f5; }
|
||||
.footer { text-align: center; color: #999; font-size: 12px; border-top: 1px solid #e8e8e8; background: #fff; flex-shrink: 0; padding: 8px 24px; }
|
||||
.footer-content { display: flex; justify-content: center; align-items: center; gap: 14px; flex-wrap: wrap; }
|
||||
.footer-links { display: flex; gap: 14px; align-items: center; }
|
||||
.footer-links a { color: #999; text-decoration: none; }
|
||||
.footer-links a:hover { color: #1890ff; }
|
||||
.gongan-link { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.gongan-icon { height: 14px; vertical-align: middle; }
|
||||
.credit-btn { display: flex; align-items: center; gap: 4px; color: #e6a23c !important; font-weight: 600; }
|
||||
.credit-text { font-size: 13px; }
|
||||
|
||||
/* ===== Desktop: > 1024px ===== */
|
||||
@media (min-width: 1025px) {
|
||||
.dt-only { display: inline-flex; }
|
||||
.sidebar-header { padding: 0 16px; }
|
||||
.sidebar.collapsed .sidebar-header { justify-content: center; }
|
||||
.sidebar.collapsed .sidebar-header .logo { font-size: 16px; text-align: center; }
|
||||
}
|
||||
|
||||
/* ===== Tablet: 769-1024px ===== */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.sidebar { width: 64px; }
|
||||
.sidebar-header { padding: 0; justify-content: center; }
|
||||
.sidebar-header .logo { font-size: 16px; flex: none; }
|
||||
.sidebar-header-actions .dt-only { display: none; }
|
||||
.menu-btn { display: inline-flex; }
|
||||
.content { padding: 20px; }
|
||||
}
|
||||
|
||||
/* ===== Mobile: < 769px ===== */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { position: fixed; left: -220px; top: 0; bottom: 0; z-index: 1000; transition: left 0.3s; }
|
||||
.sidebar.mobile-show { left: 0; }
|
||||
.sidebar {
|
||||
position: fixed; left: -220px; top: 0; bottom: 0; z-index: 1000;
|
||||
transition: left 0.3s; width: 220px;
|
||||
}
|
||||
.sidebar.collapsed { width: 220px; }
|
||||
.sidebar.mobile-show { left: 0; }
|
||||
.sidebar.collapsed.mobile-show { left: 0; }
|
||||
.sidebar-mask { display: block; position: fixed; inset: 0; background: rgba(0,0,0,0.3); z-index: 999; opacity: 0; pointer-events: none; transition: opacity 0.3s; }
|
||||
.sidebar-header { padding: 0 16px; }
|
||||
.sidebar-header .logo { font-size: 18px; flex: 1; }
|
||||
.sidebar-header-actions .dt-only { display: none; }
|
||||
.sidebar-mask {
|
||||
display: block; position: fixed; inset: 0; background: rgba(0,0,0,0.3);
|
||||
z-index: 999; opacity: 0; pointer-events: none; transition: opacity 0.3s;
|
||||
}
|
||||
.sidebar-mask:has(+ .sidebar.mobile-show) { opacity: 1; pointer-events: auto; }
|
||||
.menu-btn { display: inline-flex; }
|
||||
.user-name { display: none; }
|
||||
.content { padding: 16px; }
|
||||
.breadcrumb :deep(.el-breadcrumb__inner) { max-width: 80px; }
|
||||
}
|
||||
|
||||
/* ===== Tiny: < 480px ===== */
|
||||
@media (max-width: 480px) {
|
||||
.topbar-right { gap: 4px; }
|
||||
.credit-text { display: none; }
|
||||
.content { padding: 12px; }
|
||||
.breadcrumb :deep(.el-breadcrumb__inner) { max-width: 60px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "Workspace",
|
||||
"home": "Home",
|
||||
"more": "More",
|
||||
"productsQuotations": "Products & Quotes",
|
||||
"discovery": "Find Buyers",
|
||||
"workspace": "Dashboard",
|
||||
"customers": "Customers",
|
||||
"products": "Products",
|
||||
"quotations": "Quotations",
|
||||
"translate": "Translate",
|
||||
"marketing": "Marketing",
|
||||
"followup": "Follow-ups",
|
||||
"biz": "Business",
|
||||
"analytics": "Analytics",
|
||||
"team": "Team",
|
||||
"notifications": "Notifications",
|
||||
"profile": "Profile",
|
||||
"credits": "Buy Credits",
|
||||
"settings": "Settings"
|
||||
"settings": "Settings",
|
||||
"customerList": "Customers",
|
||||
"discovery": "Find Buyers",
|
||||
"followup": "Follow-ups",
|
||||
"translate": "Translate",
|
||||
"products": "Products",
|
||||
"quotations": "Quotations",
|
||||
"marketing": "Marketing"
|
||||
},
|
||||
"topbar": {
|
||||
"credits": "credits",
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "首页工作台",
|
||||
"home": "首页",
|
||||
"more": "更多",
|
||||
"productsQuotations": "报价产品",
|
||||
"discovery": "发现客户",
|
||||
"workspace": "工作台",
|
||||
"customers": "客户管理",
|
||||
"products": "产品库",
|
||||
"quotations": "报价单",
|
||||
"translate": "智能翻译",
|
||||
"marketing": "营销素材",
|
||||
"followup": "跟进提醒",
|
||||
"customers": "客户工作台",
|
||||
"biz": "业务工作台",
|
||||
"analytics": "数据分析",
|
||||
"team": "团队协作",
|
||||
"notifications": "通知中心",
|
||||
"profile": "个人中心",
|
||||
"credits": "购买次数",
|
||||
"settings": "设置"
|
||||
"settings": "设置",
|
||||
"customerList": "客户列表",
|
||||
"discovery": "发现客户",
|
||||
"followup": "跟进提醒",
|
||||
"translate": "智能翻译",
|
||||
"products": "产品库",
|
||||
"quotations": "报价单",
|
||||
"marketing": "营销素材"
|
||||
},
|
||||
"topbar": {
|
||||
"credits": "次",
|
||||
|
||||
@@ -8,11 +8,9 @@ const routes = [
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ 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: '', name: 'Home', component: () => import('@/views/NewHome.vue'), meta: { title: '首页' } },
|
||||
{ path: 'customers', name: 'Customers', component: () => import('@/views/WorkspaceCustomer.vue'), meta: { title: '客户工作台' } },
|
||||
{ path: 'biz', name: 'Biz', component: () => import('@/views/WorkspaceBiz.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: '个人中心' } },
|
||||
@@ -23,11 +21,11 @@ const routes = [
|
||||
{ path: 'profile/feedback', name: 'Feedback', component: () => import('@/views/Feedback.vue'), meta: { title: '意见反馈' } },
|
||||
]
|
||||
},
|
||||
// Redirect old top-level routes to new /workspace children
|
||||
// Consolidated redirects - old feature pages → new workspace pages
|
||||
{ path: '/customers', redirect: '/workspace/customers' },
|
||||
{ path: '/products', redirect: '/workspace/products' },
|
||||
{ path: '/quotations', redirect: '/workspace/quotations' },
|
||||
{ path: '/translate', redirect: '/workspace/translate' },
|
||||
{ path: '/products', redirect: '/workspace/biz' },
|
||||
{ path: '/quotations', redirect: '/workspace/biz' },
|
||||
{ path: '/translate', redirect: '/workspace/biz' },
|
||||
{ path: '/analytics', redirect: '/workspace/analytics' },
|
||||
{ path: '/team', redirect: '/workspace/team' },
|
||||
{ path: '/profile', redirect: '/workspace/profile' },
|
||||
@@ -37,33 +35,13 @@ const routes = [
|
||||
{ path: '/notifications', redirect: '/workspace/profile/notifications' },
|
||||
{ path: '/feedback', redirect: '/workspace/profile/feedback' },
|
||||
{ path: '/agent', redirect: '/workspace' },
|
||||
{ path: '/discovery', redirect: '/workspace/customers' },
|
||||
{ path: '/followup', redirect: '/workspace/customers' },
|
||||
{ path: '/marketing', redirect: '/workspace/biz' },
|
||||
{ path: '/workspace/home', redirect: '/workspace' },
|
||||
|
||||
// Hidden routes – still accessible via direct URL but not in sidebar
|
||||
{
|
||||
path: '/discovery',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Discovery', component: () => import('@/views/Discovery.vue'), meta: { title: '发现客户' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/marketing',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Marketing', component: () => import('@/views/Marketing.vue'), meta: { title: '营销素材' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/followup',
|
||||
component: () => import('@/layouts/UserLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Followup', component: () => import('@/views/Followup.vue'), meta: { title: '智能跟进' } },
|
||||
]
|
||||
},
|
||||
{ path: '/workspace/products', redirect: '/workspace/biz' },
|
||||
{ path: '/workspace/quotations', redirect: '/workspace/biz' },
|
||||
{ path: '/workspace/translate', redirect: '/workspace/biz' },
|
||||
{ path: '/:pathMatch(.*)*', redirect: '/' },
|
||||
]
|
||||
|
||||
|
||||
+126
-530
@@ -1,61 +1,20 @@
|
||||
<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 -->
|
||||
<!-- Welcome + Credit -->
|
||||
<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>
|
||||
<p class="welcome-desc">{{ $t('home.subtitle') || '按 ⌘K 打开 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>
|
||||
<span class="credit-amount">{{ creditBalance ?? '...' }}<small> {{ $t('topbar.credits') }}</small></span>
|
||||
<el-button size="small" round @click="$router.push('/workspace/profile/credits')">{{ $t('home.buyCredits') || '购买' }} →</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<!-- Overview Stats -->
|
||||
<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)">
|
||||
@@ -65,23 +24,32 @@
|
||||
</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>
|
||||
<!-- Quick Actions -->
|
||||
<el-card shadow="never" class="quick-section">
|
||||
<template #header><span class="section-title">{{ $t('home.quickActions') || '快捷操作' }}</span></template>
|
||||
<div class="quick-grid">
|
||||
<el-button size="large" class="quick-btn" @click="$router.push('/workspace/customers')">
|
||||
<el-icon :size="24"><User /></el-icon>
|
||||
<span>{{ $t('nav.customers') }}</span>
|
||||
</el-button>
|
||||
<el-button size="large" class="quick-btn" @click="$router.push('/workspace/biz')">
|
||||
<el-icon :size="24"><ChatLineSquare /></el-icon>
|
||||
<span>{{ $t('nav.translate') }}</span>
|
||||
</el-button>
|
||||
<el-button size="large" class="quick-btn" @click="$router.push('/workspace/biz')">
|
||||
<el-icon :size="24"><DocumentCopy /></el-icon>
|
||||
<span>{{ $t('nav.quotations') }}</span>
|
||||
</el-button>
|
||||
<el-button size="large" class="quick-btn" @click="$router.push('/workspace/biz')">
|
||||
<el-icon :size="24"><EditPen /></el-icon>
|
||||
<span>{{ $t('nav.marketing') }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- Activity Feed (Wave 2 placeholder) -->
|
||||
<!-- Recent Activity -->
|
||||
<el-card shadow="never" class="activity-section" v-if="recentActivities.length">
|
||||
<template #header>
|
||||
<span class="section-title">{{ $t('home.recentActivity') || '最近活动' }}</span>
|
||||
</template>
|
||||
<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>
|
||||
@@ -91,366 +59,81 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- Pipeline List -->
|
||||
<div class="agent-section">
|
||||
<!-- Agent Pipelines Summary -->
|
||||
<el-card shadow="never" class="agent-section" v-if="pipelines.length">
|
||||
<template #header>
|
||||
<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"
|
||||
/>
|
||||
<span class="section-title">{{ $t('agent.pipeline') || '数字员工任务' }}</span>
|
||||
<el-button size="small" type="primary" link @click="$router.push('/workspace')">{{ $t('home.viewAll') || '查看全部' }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-empty v-else :description="$t('agent.noTasks') || '暂无任务,点击上方新建'" :image-size="80" />
|
||||
<div class="pipeline-mini-list">
|
||||
<div v-for="p in pipelines.slice(0, 5)" :key="p.id" class="pipeline-mini-item" @click="selectPipeline(p)">
|
||||
<div class="pipeline-mini-left">
|
||||
<strong>{{ p.product_name }}</strong>
|
||||
<span class="pipeline-mini-market">{{ p.target_market }}</span>
|
||||
</div>
|
||||
<el-tag :type="statusTag(p.status)" size="small">{{ statusLabel(p.status) }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pipeline Detail -->
|
||||
<el-card v-if="selectedPipeline" shadow="never" class="pipeline-detail">
|
||||
<template #header>
|
||||
<div v-if="selectedPipeline" class="pipeline-detail-compact">
|
||||
<el-divider />
|
||||
<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>
|
||||
<el-button size="small" text @click="selectedPipeline = null">{{ $t('common.close') || '关闭' }}</el-button>
|
||||
</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">
|
||||
<div v-for="(st, stKey) in selectedPipeline.pipeline_data?.stages || {}" :key="stKey" :class="['stage-step', st.status]">
|
||||
<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>
|
||||
<span>{{ stageLabel(stKey) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="leads.length" class="leads-mini">
|
||||
<span v-for="lead in leads.slice(0, 3)" :key="lead.name" class="lead-chip">
|
||||
{{ lead.name }}
|
||||
<el-tag v-if="lead.match_score >= 70" size="small" type="success">{{ lead.match_score }}%</el-tag>
|
||||
</span>
|
||||
</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()
|
||||
import { User, ChatLineSquare, DocumentCopy, EditPen, CircleCheck, CircleClose, Loading } from '@element-plus/icons-vue'
|
||||
import { getCreditBalance, getAnalyticsOverview, listAgentPipelines, getAgentPipeline } from '@/api'
|
||||
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 leads = computed(() => selectedPipeline.value?.pipeline_data?.leads || [])
|
||||
|
||||
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} 个线索`,
|
||||
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}%`,
|
||||
id: `p-${p.id}`, type: 'warning', tag: 'Agent',
|
||||
text: `${p.product_name} — ${p.target_market},${p.progress || 0}%`,
|
||||
time: formatTime(p.created_at),
|
||||
})
|
||||
}
|
||||
@@ -458,118 +141,34 @@ const recentActivities = computed(() => {
|
||||
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
|
||||
return { running: t('agent.running') || '进行中', completed: t('agent.done') || '已完成', failed: t('agent.failed') || '失败' }[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'
|
||||
return { discover: '搜索', analyze: '分析', outreach: '触达', complete: '完成' }[key] || key
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
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')
|
||||
if (res.code === 0) selectedPipeline.value = res.data
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loadPipelines()
|
||||
try {
|
||||
const balance = await getCreditBalance()
|
||||
creditBalance.value = balance.balance
|
||||
@@ -579,101 +178,98 @@ onMounted(async () => {
|
||||
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' },
|
||||
{ value: d.translations?.today || 0, label: t('home.todayTranslate') || '今日翻译', color: '#52c41a', route: '/workspace/biz' },
|
||||
{ value: d.quotations?.total || 0, label: t('home.totalQuotations') || '报价单数', color: '#faad14', route: '/workspace/biz' },
|
||||
{ value: creditBalance.value ?? 0, label: t('home.credits') || '可用次数', color: '#e6a23c', route: '/workspace/profile/credits' },
|
||||
]
|
||||
} catch { /* ignore */ }
|
||||
try {
|
||||
const res = await listAgentPipelines({ page: 1, size: 10 })
|
||||
if (res.code === 0) pipelines.value = res.data.items || []
|
||||
} catch { /* ignore */ }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-dashboard { max-width: 1200px; margin: 0 auto; }
|
||||
.home-dashboard { max-width: 900px; 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-section {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin-bottom: 20px; gap: 16px; flex-wrap: wrap;
|
||||
}
|
||||
.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; }
|
||||
.credit-card { background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; flex-shrink: 0; }
|
||||
.credit-inner { display: flex; align-items: center; gap: 16px; padding: 4px 0; }
|
||||
.credit-amount { font-size: 24px; font-weight: 700; }
|
||||
.credit-amount small { font-size: 13px; 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; }
|
||||
.quick-section { margin-bottom: 20px; }
|
||||
.section-title { font-weight: 600; font-size: 15px; }
|
||||
.activity-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.quick-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
|
||||
.quick-btn {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
||||
height: auto !important; padding: 20px 8px !important; border-radius: 8px;
|
||||
color: #333; border-color: #e8e8e8;
|
||||
}
|
||||
.quick-btn:hover { color: #667eea; border-color: #667eea; }
|
||||
|
||||
.activity-section { margin-bottom: 20px; }
|
||||
.activity-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.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; }
|
||||
.section-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.pipeline-mini-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.pipeline-mini-item {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 10px 12px; border-radius: 6px; cursor: pointer; transition: background 0.15s;
|
||||
}
|
||||
.pipeline-mini-item:hover { background: #f5f7fa; }
|
||||
.pipeline-mini-left { display: flex; gap: 8px; align-items: center; font-size: 13px; }
|
||||
.pipeline-mini-left strong { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pipeline-mini-market { color: #409eff; font-size: 12px; }
|
||||
.pipeline-detail-compact { margin-top: 8px; }
|
||||
.detail-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 14px; }
|
||||
.stage-steps { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.stage-step {
|
||||
display: flex; align-items: center; gap: 6px; padding: 6px 12px;
|
||||
border-radius: 6px; background: #f5f7fa; font-size: 12px;
|
||||
}
|
||||
.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; }
|
||||
.leads-mini { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.lead-chip { display: flex; align-items: center; gap: 4px; padding: 4px 10px; background: #f5f7fa; border-radius: 4px; font-size: 12px; }
|
||||
|
||||
@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%; }
|
||||
.home-dashboard { padding: 0; }
|
||||
.quick-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; }
|
||||
.quick-btn { padding: 14px 6px !important; }
|
||||
.welcome-section { flex-direction: column; align-items: stretch; }
|
||||
.welcome-left h2 { font-size: 20px; }
|
||||
.stat-value { font-size: 22px; }
|
||||
.pipeline-mini-left { flex-direction: column; align-items: flex-start; gap: 2px; }
|
||||
.pipeline-mini-left strong { max-width: 140px; }
|
||||
.stage-steps { gap: 4px; }
|
||||
.stage-step { padding: 4px 8px; font-size: 11px; }
|
||||
.activity-text { max-width: 140px; }
|
||||
.activity-item { flex-wrap: wrap; gap: 4px; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.quick-grid { grid-template-columns: repeat(2, 1fr); gap: 6px; }
|
||||
.quick-btn { padding: 12px 4px !important; font-size: 12px; }
|
||||
.credit-amount { font-size: 20px; }
|
||||
.welcome-left h2 { font-size: 18px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="wb">
|
||||
<div class="wb-header">
|
||||
<h2>{{ $t('nav.biz') }}</h2>
|
||||
<el-button-group class="wb-tabs">
|
||||
<el-button
|
||||
v-for="tab in tabs" :key="tab.key"
|
||||
:type="activeTab === tab.key ? 'primary' : 'default'"
|
||||
size="small"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
<el-icon :size="14"><component :is="tab.icon" /></el-icon>
|
||||
{{ tab.label }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'translate'" class="wb-panel">
|
||||
<Translate ref="translateRef" />
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'products'" class="wb-panel">
|
||||
<Products ref="productsRef" />
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'quotations'" class="wb-panel">
|
||||
<Quotations ref="quotationsRef" />
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'marketing'" class="wb-panel">
|
||||
<Marketing ref="marketingRef" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Translate from '@/views/Translate.vue'
|
||||
import Products from '@/views/Products.vue'
|
||||
import Quotations from '@/views/Quotations.vue'
|
||||
import Marketing from '@/views/Marketing.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const activeTab = ref('translate')
|
||||
|
||||
const tabs = [
|
||||
{ key: 'translate', label: t('nav.translate'), icon: 'ChatLineSquare' },
|
||||
{ key: 'products', label: t('nav.products'), icon: 'Goods' },
|
||||
{ key: 'quotations', label: t('nav.quotations'), icon: 'DocumentCopy' },
|
||||
{ key: 'marketing', label: t('nav.marketing'), icon: 'EditPen' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wb { height: 100%; display: flex; flex-direction: column; }
|
||||
.wb-header {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
margin-bottom: 16px; flex-shrink: 0; flex-wrap: wrap;
|
||||
}
|
||||
.wb-header h2 { margin: 0; font-size: 18px; font-weight: 600; color: #303133; white-space: nowrap; }
|
||||
.wb-panel { flex: 1; min-height: 0; }
|
||||
|
||||
.wb-tabs { flex-wrap: wrap; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.wb-header { flex-direction: column; align-items: stretch; gap: 8px; }
|
||||
.wb-header h2 { font-size: 16px; }
|
||||
.wb-tabs { display: flex; flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; gap: 4px; }
|
||||
.wb-tabs::-webkit-scrollbar { display: none; }
|
||||
.wb-tabs .el-button { flex-shrink: 0; }
|
||||
.wb-panel { margin: 0 -12px; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="wc">
|
||||
<div class="wc-header">
|
||||
<h2>{{ $t('nav.customers') }}</h2>
|
||||
<el-button-group class="wc-tabs">
|
||||
<el-button
|
||||
v-for="tab in tabs" :key="tab.key"
|
||||
:type="activeTab === tab.key ? 'primary' : 'default'"
|
||||
size="small"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
<el-icon :size="14"><component :is="tab.icon" /></el-icon>
|
||||
{{ tab.label }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Customer List -->
|
||||
<div v-show="activeTab === 'list'" class="wc-panel">
|
||||
<Customers ref="customersRef" />
|
||||
</div>
|
||||
|
||||
<!-- Tab: Discovery -->
|
||||
<div v-show="activeTab === 'discovery'" class="wc-panel">
|
||||
<Discovery ref="discoveryRef" />
|
||||
</div>
|
||||
|
||||
<!-- Tab: Followups -->
|
||||
<div v-show="activeTab === 'followup'" class="wc-panel">
|
||||
<Followup ref="followupRef" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Customers from '@/views/Customers.vue'
|
||||
import Discovery from '@/views/Discovery.vue'
|
||||
import Followup from '@/views/Followup.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const activeTab = ref('list')
|
||||
|
||||
const tabs = [
|
||||
{ key: 'list', label: t('nav.customerList'), icon: 'User' },
|
||||
{ key: 'discovery', label: t('nav.discovery'), icon: 'Search' },
|
||||
{ key: 'followup', label: t('nav.followup'), icon: 'AlarmClock' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wc { height: 100%; display: flex; flex-direction: column; }
|
||||
.wc-header {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
margin-bottom: 16px; flex-shrink: 0; flex-wrap: wrap;
|
||||
}
|
||||
.wc-header h2 { margin: 0; font-size: 18px; font-weight: 600; color: #303133; white-space: nowrap; }
|
||||
.wc-panel { flex: 1; min-height: 0; }
|
||||
|
||||
/* Tabs: scrollable on narrow screens, wrap on tablets */
|
||||
.wc-tabs { flex-wrap: wrap; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.wc-header { flex-direction: column; align-items: stretch; gap: 8px; }
|
||||
.wc-header h2 { font-size: 16px; }
|
||||
.wc-tabs { display: flex; flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; gap: 4px; }
|
||||
.wc-tabs::-webkit-scrollbar { display: none; }
|
||||
.wc-tabs .el-button { flex-shrink: 0; }
|
||||
.wc-panel { margin: 0 -12px; /* span full bleed on mobile */ }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user