feat: restructure pricing models, add upgrade modal and ecosystem UI

This commit is contained in:
wlt
2026-06-24 10:38:11 +08:00
parent 7b03d803b5
commit eb39cc1baa
7 changed files with 782 additions and 111 deletions
+4
View File
@@ -18,6 +18,10 @@ http.interceptors.response.use(
const path = window.location.pathname.replace('/workspace', '') || '/' const path = window.location.pathname.replace('/workspace', '') || '/'
window.location.href = '/workspace/login?redirect=' + encodeURIComponent(path) window.location.href = '/workspace/login?redirect=' + encodeURIComponent(path)
} }
if (err.response?.status === 402) {
const detail = err.response?.data?.detail || '次数不足'
window.dispatchEvent(new CustomEvent('trademate:upgrade', { detail: { message: detail } }))
}
return Promise.reject(err.response?.data || err) return Promise.reject(err.response?.data || err)
} }
) )
@@ -0,0 +1,242 @@
<template>
<teleport to="body">
<transition name="modal-fade">
<div v-if="visible" class="upgrade-overlay" @click.self="close">
<div class="upgrade-modal">
<div class="modal-head">
<h2>{{ title || '升级套餐' }}</h2>
<p class="modal-sub" v-if="message">{{ message }}</p>
<button class="modal-x" @click="close">&times;</button>
</div>
<div class="modal-plans" v-loading="loading">
<div
v-for="p in displayPlans"
:key="p.id"
class="plan-card"
:class="{ featured: p.featured, current: p.isCurrent }"
>
<div v-if="p.badge" class="plan-badge">{{ p.badge }}</div>
<div class="plan-name">{{ p.name }}</div>
<div class="plan-name-en">{{ p.name_en }}</div>
<div class="plan-price">
<template v-if="p.price > 0">
¥{{ p.price }}<small>/</small>
</template>
<span v-else class="plan-free">免费</span>
</div>
<div v-if="p.credits" class="plan-credits">{{ p.credits }} <small>积分/</small></div>
<ul class="plan-features">
<li v-for="f in p.features" :key="f">{{ f }}</li>
</ul>
<el-button
v-if="p.isCurrent"
type="default"
disabled
class="plan-btn"
>当前套餐</el-button>
<el-button
v-else-if="p.price === 0"
type="default"
disabled
class="plan-btn"
>当前套餐</el-button>
<el-button
v-else
type="primary"
class="plan-btn"
:loading="payingId === p.id"
@click="handleUpgrade(p)"
>升级</el-button>
</div>
</div>
<div class="modal-foot">
<span class="hint">订阅后网页端浏览器插件Skills 通用</span>
<el-button text size="small" @click="goCreditsPage">购买积分包低至 ¥2.9</el-button>
</div>
</div>
</div>
</transition>
</teleport>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { getSubscriptionPlans, subscribeCreditPlan } from '@/api'
const props = defineProps({
visible: { type: Boolean, default: false },
title: { type: String, default: '' },
message: { type: String, default: '' },
})
const emit = defineEmits(['update:visible'])
const router = useRouter()
const loading = ref(false)
const plans = ref([])
const currentPlanId = ref(null)
const payingId = ref(null)
const FEATURES_FALLBACK = {
'free': { name: 'Free', name_en: 'Free', credits: 30, price: 0, features: ['30 积分(一次性)', '每日 1000 字免费翻译', '基本功能体验'], badge: '' },
'starter': { name: 'Starter', name_en: 'Starter', credits: 200, price: 9.9, features: ['200 积分/月', '无每日限制', '翻译 + 客户发现 + 营销'], badge: '入门' },
'pro': { name: 'Professional', name_en: 'Professional', credits: 1000, price: 49, features: ['1000 积分/月', 'AI 数字员工', '团队协作(3 人)', '优先支持'], badge: '推荐' },
'enterprise': { name: 'Enterprise', name_en: 'Enterprise', credits: 2500, price: 99, features: ['2500 积分/月', '不限团队人数', 'API 调用权限', 'SLA 保障'], badge: '旗舰' },
}
const displayPlans = computed(() => {
if (plans.value.length) {
return plans.value.map(p => ({
...p,
isCurrent: p.id === currentPlanId.value,
featured: p.credits_per_month >= 500 && p.credits_per_month < 2000,
badge: p.credits_per_month >= 500 && p.credits_per_month < 2000 ? '推荐' : '',
}))
}
// Fallback display when API not loaded yet
return Object.entries(FEATURES_FALLBACK).map(([key, v]) => ({
id: key,
...v,
isCurrent: key === 'free',
featured: key === 'pro',
badge: key === 'pro' ? '推荐' : (key === 'enterprise' ? '旗舰' : ''),
}))
})
async function loadPlans() {
loading.value = true
try {
const res = await getSubscriptionPlans()
plans.value = Array.isArray(res) ? res : (res.data || res.items || res.plans || [])
} catch { /* fallback to hardcoded */ }
loading.value = false
}
async function handleUpgrade(plan) {
payingId.value = plan.id
// For Free/current, no action
if (plan.price === 0 || plan.isCurrent) {
payingId.value = null
return
}
try {
// Use credits subscribe endpoint
const res = await subscribeCreditPlan(plan.id, 'alipay')
if (res.pay_url) {
window.open(res.pay_url, '_blank')
} else {
ElMessage.success('订阅成功!')
}
close()
} catch (e) {
const detail = e?.detail || e?.message || '订阅失败'
ElMessage.error(detail)
}
payingId.value = null
}
function goCreditsPage() {
close()
router.push('/workspace/profile/credits')
}
function close() {
emit('update:visible', false)
}
// Listen for global upgrade event (from 402 interceptor or other components)
function onUpgradeEvent(e) {
// Don't auto-show if we're on the credits page already
if (router.currentRoute?.value?.path?.includes('/credits')) return
if (router.currentRoute?.value?.path?.includes('/upgrade')) return
emit('update:visible', true)
}
watch(() => props.visible, (v) => {
if (v) loadPlans()
})
onMounted(() => {
window.addEventListener('trademate:upgrade', onUpgradeEvent)
if (props.visible) loadPlans()
})
onUnmounted(() => {
window.removeEventListener('trademate:upgrade', onUpgradeEvent)
})
</script>
<style scoped>
.upgrade-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.5);
display: flex; align-items: center; justify-content: center;
z-index: 2000;
}
.upgrade-modal {
background: #fff; border-radius: 16px; width: 640px; max-width: 94vw;
max-height: 90vh; overflow-y: auto; box-shadow: 0 16px 48px rgba(0,0,0,0.2);
animation: modalIn 0.25s ease;
}
@keyframes modalIn {
from { transform: scale(0.92) translateY(20px); opacity: 0; }
to { transform: scale(1) translateY(0); opacity: 1; }
}
.modal-head {
position: relative; padding: 20px 24px 0;
}
.modal-head h2 { margin: 0; font-size: 18px; color: #1e293b; }
.modal-sub { margin: 6px 0 0; font-size: 13px; color: #dc2626; }
.modal-x {
position: absolute; right: 20px; top: 16px;
background: none; border: none; font-size: 24px; color: #94a3b8; cursor: pointer;
}
.modal-x:hover { color: #64748b; }
.modal-plans {
padding: 20px 24px; display: flex; gap: 12px;
min-height: 260px;
}
.plan-card {
flex: 1; border: 1px solid #e5e7eb; border-radius: 12px;
padding: 16px; text-align: center; position: relative;
transition: border-color 0.2s, box-shadow 0.2s;
}
.plan-card:hover {
border-color: #2563eb; box-shadow: 0 2px 12px rgba(37,99,235,0.08);
}
.plan-card.featured {
border-color: #2563eb; border-width: 2px; background: #f8faff;
transform: scale(1.04);
}
.plan-card.current {
border-color: #52c41a; background: #f6ffed;
}
.plan-badge {
position: absolute; top: -10px; left: 50%; transform: translateX(-50%);
background: #2563eb; color: #fff; font-size: 11px;
padding: 2px 12px; border-radius: 10px; font-weight: 600; white-space: nowrap;
}
.plan-card.current .plan-badge { background: #52c41a; }
.plan-name { font-size: 15px; font-weight: 700; color: #1e293b; margin-top: 4px; }
.plan-name-en { font-size: 11px; color: #94a3b8; margin-bottom: 6px; }
.plan-price { font-size: 24px; font-weight: 800; color: #2563eb; margin: 6px 0; }
.plan-price small { font-size: 13px; font-weight: 400; color: #64748b; }
.plan-free { font-size: 18px; color: #64748b; }
.plan-credits { font-size: 12px; color: #64748b; margin-bottom: 8px; }
.plan-features { list-style: none; padding: 0; margin: 0 0 12px; }
.plan-features li {
font-size: 12px; color: #64748b; line-height: 1.8; padding: 0;
}
.plan-features li::before { content: '✓ '; color: #52c41a; font-weight: 700; }
.plan-btn { width: 100%; }
.modal-foot {
padding: 8px 24px 16px; text-align: center; display: flex;
flex-direction: column; gap: 4px;
}
.modal-foot .hint { font-size: 12px; color: #94a3b8; }
.modal-fade-enter-active, .modal-fade-leave-active { transition: opacity 0.2s; }
.modal-fade-enter-from, .modal-fade-leave-to { opacity: 0; }
</style>
+25 -2
View File
@@ -30,6 +30,7 @@
<el-icon><Menu /></el-icon> <el-icon><Menu /></el-icon>
<span>{{ $t('nav.more') || '更多' }}</span> <span>{{ $t('nav.more') || '更多' }}</span>
</template> </template>
<el-menu-item index="/workspace/upgrade"><el-icon><TrendCharts /></el-icon><span>{{ $t('nav.upgrade') || '升级套餐' }}</span></el-menu-item>
<el-menu-item index="/workspace/profile"><el-icon><User /></el-icon><span>{{ $t('nav.profile') }}</span></el-menu-item> <el-menu-item index="/workspace/profile"><el-icon><User /></el-icon><span>{{ $t('nav.profile') }}</span></el-menu-item>
<el-menu-item index="/workspace/team"><el-icon><UserFilled /></el-icon><span>{{ $t('nav.team') }}</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-sub-menu>
@@ -47,9 +48,12 @@
</el-breadcrumb> </el-breadcrumb>
<div class="topbar-right"> <div class="topbar-right">
<el-button text style="font-size:13px;color:#999" @click="toggleLang">{{ currentLang }}</el-button> <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('/workspace/profile/credits')"> <el-button v-if="creditBalance !== null" text :class="['credit-btn', creditLow ? 'credit-low' : '']" @click="onCreditClick">
<el-icon><Coin /></el-icon> <el-icon><Coin /></el-icon>
<span class="credit-text">{{ creditBalance }} {{ $t('topbar.credits') }}</span> <span class="credit-text">
{{ creditBalance }} {{ $t('topbar.credits') }}
<span v-if="creditLow" class="credit-warn">· 升级</span>
</span>
</el-button> </el-button>
<el-badge :value="unread" :hidden="!unread" class="notif-badge"> <el-badge :value="unread" :hidden="!unread" class="notif-badge">
<el-button text style="font-size:18px" @click="$router.push('/workspace/profile/notifications')"> <el-button text style="font-size:18px" @click="$router.push('/workspace/profile/notifications')">
@@ -80,6 +84,8 @@
</div> </div>
<CommandK /> <CommandK />
<AiAssistant />
<UpgradeModal v-model:visible="showUpgradeModal" title="升级套餐" />
</div> </div>
</template> </template>
@@ -90,6 +96,8 @@ import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { getUnreadCount, getCreditBalance } from '@/api' import { getUnreadCount, getCreditBalance } from '@/api'
import CommandK from '@/components/CommandK.vue' import CommandK from '@/components/CommandK.vue'
import AiAssistant from '@/components/AiAssistant.vue'
import UpgradeModal from '@/components/UpgradeModal.vue'
import { switchLang } from '@/i18n' import { switchLang } from '@/i18n'
const route = useRoute() const route = useRoute()
@@ -100,6 +108,9 @@ const collapsed = ref(window.innerWidth < 1024)
const showMobileMenu = ref(false) const showMobileMenu = ref(false)
const unread = ref(0) const unread = ref(0)
const creditBalance = ref(null) const creditBalance = ref(null)
const showUpgradeModal = ref(false)
const creditLow = computed(() => creditBalance.value !== null && creditBalance.value < 10)
function handleResize() { function handleResize() {
const w = window.innerWidth const w = window.innerWidth
@@ -121,6 +132,14 @@ function toggleLang() {
switchLang(next) switchLang(next)
} }
function onCreditClick() {
if (creditLow.value) {
showUpgradeModal.value = true
} else {
router.push('/workspace/profile/credits')
}
}
async function loadCreditBalance() { async function loadCreditBalance() {
try { try {
const res = await getCreditBalance() const res = await getCreditBalance()
@@ -180,6 +199,10 @@ function handleLogout() {
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-shrink: 0; } .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; } .notif-badge :deep(.el-badge__content) { top: 8px; right: 4px; }
.credit-btn { display: flex; align-items: center; gap: 4px; color: #e6a23c !important; font-weight: 600; } .credit-btn { display: flex; align-items: center; gap: 4px; color: #e6a23c !important; font-weight: 600; }
.credit-btn.credit-low { color: #dc2626 !important; animation: pulse-warn 2s infinite; }
@keyframes pulse-warn { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
.credit-text { font-size: 13px; }
.credit-warn { font-size: 12px; font-weight: 400; }
/* ===== Desktop: > 1024px ===== */ /* ===== Desktop: > 1024px ===== */
@media (min-width: 1025px) { @media (min-width: 1025px) {
+1
View File
@@ -9,6 +9,7 @@ const routes = [
meta: { requiresAuth: true }, meta: { requiresAuth: true },
children: [ children: [
{ path: '', name: 'Home', component: () => import('@/views/NewHome.vue'), meta: { title: '首页' } }, { path: '', name: 'Home', component: () => import('@/views/NewHome.vue'), meta: { title: '首页' } },
{ path: 'upgrade', name: 'Upgrade', component: () => import('@/views/Upgrade.vue'), meta: { title: '升级套餐' } },
{ path: 'customers', name: 'Customers', component: () => import('@/views/WorkspaceCustomer.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: 'biz', name: 'Biz', component: () => import('@/views/WorkspaceBiz.vue'), meta: { title: '业务工作台' } },
{ path: 'analytics', name: 'Analytics', component: () => import('@/views/Analytics.vue'), meta: { title: '数据分析' } }, { path: 'analytics', name: 'Analytics', component: () => import('@/views/Analytics.vue'), meta: { title: '数据分析' } },
+85 -1
View File
@@ -14,6 +14,66 @@
</el-card> </el-card>
</div> </div>
<!-- Ecosystem -->
<el-card shadow="never" class="ecosystem-card">
<div class="eco-inner">
<div class="eco-item" @click="$router.push('/workspace/upgrade')">
<el-tag class="eco-tag" color="#fff" effect="plain" style="color:#1890ff;border-color:#1890ff">网页端</el-tag>
<span class="eco-label">全功能工作台</span>
</div>
<div class="eco-divider" />
<div class="eco-item" @click="showEcoModal = true">
<el-tag class="eco-tag" color="#fff" effect="plain" style="color:#faad14;border-color:#faad14">插件</el-tag>
<span class="eco-label">Chrome 浏览器扩展</span>
</div>
<div class="eco-divider" />
<div class="eco-item" @click="showEcoModal = true">
<el-tag class="eco-tag" color="#fff" effect="plain" style="color:#722ed1;border-color:#722ed1">技能</el-tag>
<span class="eco-label">AI 技能包 (SKILL.md)</span>
</div>
<el-button text type="primary" size="small" class="eco-more" @click="showEcoModal = true">了解更多 </el-button>
</div>
</el-card>
<!-- Ecosystem Info Modal -->
<el-dialog v-model="showEcoModal" title="TradeMate 产品生态" width="560px">
<div class="eco-modal-body">
<div class="eco-modal-item">
<div class="eco-modal-icon" style="background:#e6f7ff;color:#1890ff">
<el-icon :size="24"><Monitor /></el-icon>
</div>
<div class="eco-modal-text">
<h4>网页工作台</h4>
<p>你现在正在使用所有功能都在这里翻译客户管理营销AI 数字员工</p>
</div>
</div>
<div class="eco-modal-item">
<div class="eco-modal-icon" style="background:#fff7e6;color:#faad14">
<el-icon :size="24"><ChromeFilled /></el-icon>
</div>
<div class="eco-modal-text">
<h4>Chrome 浏览器插件</h4>
<p>划词翻译一键客户搜索快捷回复项目目录 <code>browser-extension/</code> 加载到 chrome://extensions/ 即可使用</p>
<el-button size="small" type="warning" plain @click="downloadExtension">下载插件</el-button>
</div>
</div>
<div class="eco-modal-item">
<div class="eco-modal-icon" style="background:#f0e6ff;color:#722ed1">
<el-icon :size="24"><Tools /></el-icon>
</div>
<div class="eco-modal-text">
<h4>AI 技能包 (SKILL.md)</h4>
<p>安装到 Cursor / Claude Code / OpenCode 用自然语言触发 TradeMate 能力项目目录 <code>.opencode/skills/</code></p>
<el-tag size="small" style="margin-top:4px">translate-reply</el-tag>
<el-tag size="small" style="margin-top:4px">customer-discovery</el-tag>
<el-tag size="small" style="margin-top:4px">marketing-content</el-tag>
</div>
</div>
<el-divider />
<p class="eco-modal-foot">三者共享同一账号和数据订阅任意入口即全平台可用</p>
</div>
</el-dialog>
<!-- Overview Stats --> <!-- Overview Stats -->
<el-row :gutter="16" class="stats-row"> <el-row :gutter="16" class="stats-row">
<el-col :xs="12" :sm="6" v-for="item in stats" :key="item.label"> <el-col :xs="12" :sm="6" v-for="item in stats" :key="item.label">
@@ -107,13 +167,20 @@
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { User, ChatLineSquare, DocumentCopy, EditPen, CircleCheck, CircleClose, Loading } from '@element-plus/icons-vue' import { User, ChatLineSquare, DocumentCopy, EditPen, CircleCheck, CircleClose, Loading, Monitor, ChromeFilled, Tools } from '@element-plus/icons-vue'
import { getCreditBalance, getAnalyticsOverview, listAgentPipelines, getAgentPipeline } from '@/api' import { getCreditBalance, getAnalyticsOverview, listAgentPipelines, getAgentPipeline } from '@/api'
const { t } = useI18n() const { t } = useI18n()
const auth = useAuthStore() const auth = useAuthStore()
const creditBalance = ref(null) const creditBalance = ref(null)
const stats = ref([]) const stats = ref([])
// Ecosystem modal
const showEcoModal = ref(false)
function downloadExtension() {
window.open('https://github.com/wlt/trade-assistant/tree/main/browser-extension', '_blank')
}
const pipelines = ref([]) const pipelines = ref([])
const selectedPipeline = ref(null) const selectedPipeline = ref(null)
const selectedId = ref(null) const selectedId = ref(null)
@@ -205,6 +272,23 @@ onMounted(async () => {
.credit-amount { font-size: 24px; font-weight: 700; } .credit-amount { font-size: 24px; font-weight: 700; }
.credit-amount small { font-size: 13px; font-weight: 400; opacity: 0.8; } .credit-amount small { font-size: 13px; font-weight: 400; opacity: 0.8; }
.ecosystem-card { margin-bottom: 16px; }
.eco-inner { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.eco-item { display: flex; align-items: center; gap: 6px; cursor: pointer; padding: 2px 4px; border-radius: 6px; transition: background 0.2s; }
.eco-item:hover { background: #f0f4ff; }
.eco-tag { font-weight: 600; font-size: 11px; }
.eco-label { font-size: 13px; color: #475569; white-space: nowrap; }
.eco-divider { width: 1px; height: 20px; background: #e5e7eb; }
.eco-more { margin-left: auto; }
.eco-modal-body { padding: 8px 0; }
.eco-modal-item { display: flex; gap: 14px; margin-bottom: 20px; }
.eco-modal-icon { width: 44px; height: 44px; border-radius: 12px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.eco-modal-text h4 { margin: 0 0 4px; font-size: 15px; color: #1e293b; }
.eco-modal-text p { margin: 0; font-size: 13px; color: #64748b; line-height: 1.5; }
.eco-modal-text code { background: #f0f4ff; color: #1890ff; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
.eco-modal-text .el-tag { margin-right: 4px; }
.eco-modal-foot { text-align: center; font-size: 13px; color: #94a3b8; }
.stats-row { margin-bottom: 20px; } .stats-row { margin-bottom: 20px; }
.stat-card { cursor: pointer; text-align: center; transition: all 0.25s; } .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-card:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,0.08); }
+276 -108
View File
@@ -1,137 +1,305 @@
<template> <template>
<div> <div class="upgrade-page">
<el-row :gutter="20"> <div class="page-head">
<el-col :xs="24" :sm="8" v-for="p in plans" :key="p.id"> <h1>选择适合你的套餐</h1>
<el-card shadow="hover" :class="{ 'plan-highlight': p.id === currentPlan, 'plan-yearly': p.period === 'year' }"> <p class="page-sub">订阅后所有产品线通用 网页工作台浏览器插件Agent Skills</p>
<template #header> <div class="billing-toggle">
<div style="text-align:center"> <el-radio-group v-model="billingPeriod" size="small">
<el-tag v-if="p.period === 'year'" type="success" size="small" style="margin-bottom:8px">年付省 {{ (p.original_price || p.price * 12) - p.price }} </el-tag> <el-radio-button value="monthly">月付</el-radio-button>
<h3 style="margin:0">{{ p.name }}</h3> <el-radio-button value="yearly">年付 <span class="save-tag" v-if="billingPeriod === 'yearly'"> 2 个月</span></el-radio-button>
<p style="font-size:28px;font-weight:700;color:#1890ff;margin:12px 0">
¥{{ p.price }}<span style="font-size:14px;font-weight:400;color:#999">/{{ p.period === 'year' ? '年' : '月' }}</span>
</p>
<p v-if="p.original_price" style="font-size:12px;color:#999;margin:-8px 0 0">
<del>¥{{ p.original_price }}/</del>{{ Math.round((1 - p.price / p.original_price) * 100) }}% 优惠
</p>
</div>
</template>
<div>
<p v-for="f in p.features || []" :key="f" style="font-size:13px;color:#666;margin:8px 0">
<el-icon color="#52c41a" style="margin-right:6px"><Check /></el-icon>{{ f }}
</p>
</div>
<div style="text-align:center;margin-top:16px">
<el-button v-if="p.id === currentPlan" type="default" disabled>当前套餐</el-button>
<el-button v-else-if="p.id === 'free'" @click="handleFree">当前套餐</el-button>
<el-button v-else type="primary" :loading="loadingId === p.id" @click="showPayDialog(p.id)">{{ p.price === 0 ? '当前套餐' : '升级' }}</el-button>
</div>
</el-card>
</el-col>
</el-row>
<el-empty v-if="!plans.length" description="暂无套餐信息" />
<el-dialog v-model="payDialog.visible" title="选择支付方式" width="400px" :close-on-click-modal="false">
<div style="text-align:center;padding:20px 0" v-if="!payDialog.orderCreated">
<el-radio-group v-model="payDialog.payType" style="margin-bottom:24px">
<el-radio-button value="alipay">
<span style="display:flex;align-items:center;gap:6px;padding:0 20px">
<svg viewBox="0 0 24 24" width="20" height="20" fill="#1677ff"><path d="M21.422 15.358c-3.22-1.386-6.847-2.408-10.564-2.828 1.102-2.279 2.38-4.49 3.735-6.59H9.878c-.185-.413-.262-.912-.04-1.436.454-1.072 1.92-1.348 1.92-1.348s.162-.09.026-.207c-.137-.117-1.866-.313-2.666-.363-2.348-.155-4.99.22-5.733 1.181-1.14 1.48.067 2.925.401 3.337.337.412 1.256.498 1.256.498s-1.466.536-1.992 1.2c-.525.665-.264 1.383.13 1.664.394.281.756.388 1.07.482.707.21 1.818.431 2.795.555 1.454.184 2.957.1 4.312-.184 1.408-2.06 2.83-4.017 4.285-5.907l3.192 1.558c.289.142.66.028.827-.256a.63.63 0 0 0-.086-.74L15.734 7.56c.7-.878 1.426-1.727 2.18-2.537 1.938-2.083 4.298-3.876 6.377-4.707a12.29 12.29 0 0 0-6.648-1.99c-6.427 0-11.66 4.996-11.66 11.116 0 1.49.294 2.913.825 4.215-.374.314-.707.674-.99 1.075-2.316 3.277-.477 6.101 1.046 7.247 1.518 1.144 4.464 1.772 7.155.875 2.798-.93 5.256-3.103 6.822-5.531 1.654-2.563 2.549-5.435 2.549-8.367a12.9 12.9 0 0 0-.316-2.81c-1.178-.022-3.226.306-5.354 1.522z"/></svg>
支付宝
</span>
</el-radio-button>
<el-radio-button value="wechat">
<span style="display:flex;align-items:center;gap:6px;padding:0 20px">
<svg viewBox="0 0 24 24" width="20" height="20" fill="#07c160"><path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.271.271 0 0 0 .14.045c.134 0 .24-.11.24-.245 0-.06-.024-.12-.04-.178l-.325-1.233a.49.49 0 0 1 .178-.553C23.028 18.125 24 16.539 24 14.711c0-3.396-3.637-6.02-7.062-5.853zm-2.06 1.964c.535 0 .968.44.968.982a.975.975 0 0 1-.968.983.975.975 0 0 1-.969-.983c0-.542.434-.982.969-.982zm4.844 0c.535 0 .969.44.969.982a.975.975 0 0 1-.969.983.975.975 0 0 1-.968-.983c0-.542.433-.982.968-.982z"/></svg>
微信支付
</span>
</el-radio-button>
</el-radio-group> </el-radio-group>
<div> </div>
<el-button type="primary" size="large" :loading="payDialog.loading" @click="handleUpgrade">立即支付</el-button> </div>
<div class="plans-grid" v-loading="loading">
<!-- Free Tier -->
<div class="plan-card" :class="{ current: currentPlan === 'free' }">
<div class="plan-name">Free</div>
<div class="plan-price free">免费</div>
<div class="plan-credits">30 积分一次性</div>
<ul class="plan-features">
<li>每日 1000 字免费翻译</li>
<li>基本功能体验</li>
<li>用完即止</li>
</ul>
<el-button type="default" disabled class="plan-btn" v-if="currentPlan === 'free'">当前套餐</el-button>
<el-button type="default" disabled class="plan-btn" v-else>当前套餐</el-button>
</div>
<!-- Dynamically loaded plan cards -->
<div
v-for="p in planCards"
:key="p.id"
class="plan-card"
:class="{
featured: p.featured,
current: p.isCurrent,
'yearly-active': billingPeriod === 'yearly'
}"
>
<div v-if="p.badge" class="plan-badge">{{ p.badge }}</div>
<div class="plan-name">{{ p.name }}</div>
<div class="plan-name-en">{{ p.name_en }}</div>
<div class="plan-price">
¥{{ billingPeriod === 'yearly' ? p.yearlyPrice : p.price }}
<small>/{{ billingPeriod === 'yearly' ? '年' : '月' }}</small>
</div>
<div v-if="billingPeriod === 'yearly' && p.yearlyOriginal" class="plan-original">
<del>¥{{ p.yearlyOriginal }}/</del>
<span class="plan-discount">{{ p.discountPct }}% 优惠</span>
</div>
<div class="plan-credits">{{ p.credits }} <small>积分/</small></div>
<ul class="plan-features">
<li v-for="f in p.features" :key="f">{{ f }}</li>
</ul>
<el-button
v-if="p.isCurrent"
type="default"
disabled
class="plan-btn"
>当前套餐</el-button>
<el-button
v-else
type="primary"
class="plan-btn"
:loading="payingId === p.id"
@click="handleUpgrade(p)"
>升级到 {{ p.name }}</el-button>
</div>
</div>
<!-- Feature Comparison Table -->
<el-card class="comparison-card" v-if="planCards.length">
<template #header><strong>完整功能对比</strong></template>
<el-table :data="comparisonRows" border stripe>
<el-table-column prop="feature" label="功能" width="160" />
<el-table-column prop="free" label="Free" width="120" align="center" />
<el-table-column v-for="p in planCards" :key="p.id" :prop="p.id" :label="p.name" width="130" align="center" />
</el-table>
</el-card>
<!-- Package section -->
<el-card class="package-card">
<template #header><strong>积分包无需订阅按需购买</strong></template>
<div class="package-grid">
<div v-for="pkg in packages" :key="pkg.id" class="package-item">
<div class="pkg-name">{{ pkg.name }}</div>
<div class="pkg-credits">{{ pkg.credits }} <small>积分</small></div>
<div class="pkg-price">¥{{ pkg.price }}</div>
<div class="pkg-unit"> ¥{{ (pkg.price / pkg.credits).toFixed(2) }}/积分</div>
<el-button size="small" type="primary" @click="buyPackage(pkg)">购买</el-button>
</div> </div>
</div> </div>
<div style="text-align:center;padding:20px 0" v-else> </el-card>
<div v-if="payDialog.codeUrl">
<p style="margin-bottom:16px;color:#666">请使用微信扫描下方二维码支付</p> <!-- Purchase Dialog -->
<img :src="payDialog.codeUrl" style="width:200px;height:200px;border:1px solid #eee;border-radius:8px" /> <el-dialog v-model="payDialog.visible" title="选择支付方式" width="360px">
<p style="margin-top:12px;font-size:12px;color:#999">支付成功后自动生效</p> <p style="margin-bottom:12px;text-align:center" v-if="payDialog.type === 'subscription'">
</div> 订阅 <strong>{{ payDialog.plan?.name }}</strong>
<div v-else-if="payDialog.payUrl"> ({{ payDialog.plan?.credits }} 积分/)
<p style="margin-bottom:16px;color:#666">正在跳转支付宝...</p> </p>
<el-button type="primary" @click="openPayUrl">前往支付</el-button> <p style="margin-bottom:12px;text-align:center" v-else>
</div> 购买 <strong>{{ payDialog.pkg?.name }}</strong> ({{ payDialog.pkg?.credits }} 积分)
<el-button style="margin-top:16px" @click="payDialog.visible = false">关闭</el-button> </p>
</div> <p style="font-size:22px;font-weight:bold;color:#e6a23c;text-align:center;margin-bottom:16px">
¥{{ payDialog.type === 'subscription' ? payDialog.plan?.price : payDialog.pkg?.price }}
</p>
<el-radio-group v-model="payDialog.payType" style="display:flex;gap:16px;justify-content:center;margin-bottom:16px">
<el-radio-button value="alipay">支付宝</el-radio-button>
<el-radio-button value="wechat">微信支付</el-radio-button>
</el-radio-group>
<template #footer>
<el-button @click="payDialog.visible = false">取消</el-button>
<el-button type="primary" @click="confirmPay" :loading="payDialog.loading">确认支付</el-button>
</template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { getPlans, getSubscription, createOrder } from '@/api'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import {
getSubscriptionPlans, getCreditPackages, getCreditBalance,
subscribeCreditPlan, purchaseCreditPackage,
} from '@/api'
const billingPeriod = ref('monthly')
const loading = ref(false)
const plans = ref([]) const plans = ref([])
const currentPlan = ref(null) const packages = ref([])
const loadingId = ref(null) const currentPlan = ref('free')
const payingId = ref(null)
const payDialog = reactive({ const payDialog = ref({
visible: false, visible: false,
planId: null, type: 'subscription', // 'subscription' | 'package'
plan: null,
pkg: null,
payType: 'alipay', payType: 'alipay',
loading: false, loading: false,
orderCreated: false,
payUrl: '',
codeUrl: '',
}) })
onMounted(async () => { const PLAN_META = {
try { starter: { badge: '入门', yearlyDiscount: 0.17, features: ['200 积分/月', '无每日限制', '翻译 + 客户发现 + 营销生成', '智能回复 + 报价单'] },
const [plansRes, subRes] = await Promise.all([getPlans(), getSubscription().catch(() => null)]) pro: { badge: '推荐', featured: true, yearlyDiscount: 0.15, features: ['1000 积分/月', 'AI 数字员工', '团队协作(3 人)', '优先技术支持'] },
const pd = plansRes.data || plansRes enterprise: { badge: '旗舰', yearlyDiscount: 0.16, features: ['2500 积分/月', '不限团队人数', 'API 调用权限', 'SLA 保障'] },
plans.value = (pd.plans || pd.items || pd || []).filter(p => p.id !== 'free')
if (subRes) {
const sd = subRes.data || subRes
currentPlan.value = sd.plan_id || sd.plan
}
} catch { /* ignore */ }
})
function showPayDialog(planId) {
payDialog.planId = planId
payDialog.payType = 'alipay'
payDialog.orderCreated = false
payDialog.payUrl = ''
payDialog.codeUrl = ''
payDialog.visible = true
} }
async function handleUpgrade() { const planCards = computed(() => {
payDialog.loading = true return plans.value.map(p => {
try { const meta = PLAN_META[p.id] || {}
const res = await createOrder(payDialog.planId, payDialog.payType) const yearlyOriginal = Math.round(p.price * 12)
payDialog.orderCreated = true const yearlyPrice = Math.round(p.price * 12 * (1 - (meta.yearlyDiscount || 0)))
if (res.code_url) { return {
payDialog.codeUrl = res.code_url ...p,
} else if (res.pay_url) { credits: p.credits_per_month || p.credits || 0,
payDialog.payUrl = res.pay_url badge: meta.badge || '',
window.open(res.pay_url) featured: meta.featured || false,
} else { isCurrent: p.id === currentPlan.value,
ElMessage.success('订单已创建,请稍后查看') yearlyPrice,
yearlyOriginal,
discountPct: Math.round((meta.yearlyDiscount || 0) * 100),
features: meta.features || [],
} }
} catch (e) { }).filter(p => p.price > 0) // exclude free
ElMessage.error(e?.detail || '下单失败') })
} finally {
payDialog.loading = false const comparisonRows = computed(() => {
const features = [
{ feature: '积分/月', free: '30(一次性)', ...Object.fromEntries(planCards.value.map(p => [p.id, p.credits])) },
{ feature: 'AI 翻译', free: '1000字/天', ...Object.fromEntries(planCards.value.map(p => [p.id, '✓'])) },
{ feature: '智能回复', free: '—', ...Object.fromEntries(planCards.value.map(p => [p.id, '✓'])) },
{ feature: '客户发现', free: '—', ...Object.fromEntries(planCards.value.map(p => [p.id, '✓'])) },
{ feature: '营销生成', free: '—', ...Object.fromEntries(planCards.value.map(p => [p.id, '✓'])) },
{ feature: '报价单', free: '—', ...Object.fromEntries(planCards.value.map(p => [p.id, '✓'])) },
{ feature: 'AI 数字员工', free: '—', ...Object.fromEntries(planCards.value.map(p => [p.id, p.credits >= 1000 ? '✓' : '—'])) },
{ feature: '团队协作', free: '—', ...Object.fromEntries(planCards.value.map(p => [p.id, p.credits >= 1000 ? '3 人' : p.credits >= 500 ? '3 人' : '—'])) },
{ feature: 'API 调用', free: '—', ...Object.fromEntries(planCards.value.map(p => [p.id, p.credits >= 2000 ? '✓' : '—'])) },
{ feature: '技术支持', free: '—', ...Object.fromEntries(planCards.value.map(p => [p.id, p.credits >= 1000 ? '优先' : '—'])) },
]
return features
})
async function loadData() {
loading.value = true
try {
const [plansRes, pkgsRes, balanceRes] = await Promise.all([
getSubscriptionPlans().catch(() => []),
getCreditPackages().catch(() => []),
getCreditBalance().catch(() => null),
])
plans.value = Array.isArray(plansRes) ? plansRes : (plansRes.data || plansRes.items || [])
packages.value = Array.isArray(pkgsRes) ? pkgsRes : (pkgsRes.data || pkgsRes.items || [])
if (balanceRes?.subscription?.plan_id) {
currentPlan.value = balanceRes.subscription.plan_id
}
} catch { /* ignore */ }
loading.value = false
}
async function handleUpgrade(plan) {
if (plan.isCurrent) return
payDialog.value = {
visible: true,
type: 'subscription',
plan,
pkg: null,
payType: 'alipay',
loading: false,
} }
} }
function openPayUrl() { async function confirmPay() {
if (payDialog.payUrl) window.open(payDialog.payUrl) const d = payDialog.value
d.loading = true
try {
if (d.type === 'subscription') {
const res = await subscribeCreditPlan(d.plan.id, d.payType)
if (res.pay_url) window.open(res.pay_url, '_blank')
else ElMessage.success('订阅成功!')
} else {
const res = await purchaseCreditPackage(d.pkg.id, d.payType)
if (res.code_url || res.pay_url) {
if (res.pay_url) window.open(res.pay_url, '_blank')
// QR code handling
if (res.code_url) {
ElMessage.info('请在新页面扫码支付')
}
} else {
ElMessage.success('购买成功!')
}
}
d.visible = false
} catch (e) {
ElMessage.error(e?.detail || e?.message || '支付失败')
}
d.loading = false
} }
function buyPackage(pkg) {
payDialog.value = {
visible: true,
type: 'package',
plan: null,
pkg,
payType: 'alipay',
loading: false,
}
}
onMounted(loadData)
</script> </script>
<style scoped> <style scoped>
.plan-highlight { border: 2px solid #1890ff; transform: scale(1.02); } .upgrade-page { max-width: 960px; margin: 0 auto; }
.plan-yearly { border: 2px solid #52c41a; } .page-head { text-align: center; margin-bottom: 32px; }
.page-head h1 { font-size: 28px; color: #1e293b; margin: 0 0 8px; }
.page-sub { font-size: 14px; color: #64748b; margin: 0 0 20px; }
.billing-toggle { display: inline-flex; align-items: center; gap: 8px; }
.save-tag { background: #52c41a; color: #fff; font-size: 10px; padding: 1px 6px; border-radius: 8px; margin-left: 4px; }
.plans-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; margin-bottom: 32px; }
.plan-card {
background: #fff; border: 1px solid #e5e7eb; border-radius: 16px;
padding: 24px 20px; text-align: center; position: relative;
transition: all 0.25s;
}
.plan-card:hover { border-color: #2563eb; box-shadow: 0 4px 16px rgba(37,99,235,0.1); transform: translateY(-2px); }
.plan-card.featured { border-color: #2563eb; border-width: 2px; background: #f8faff; }
.plan-card.current { border-color: #52c41a; background: #f6ffed; }
.plan-card.yearly-active.featured { border-color: #2563eb; box-shadow: 0 4px 20px rgba(37,99,235,0.15); }
.plan-badge {
position: absolute; top: -10px; left: 50%; transform: translateX(-50%);
background: #2563eb; color: #fff; font-size: 11px; padding: 2px 14px;
border-radius: 10px; font-weight: 600;
}
.plan-card.current .plan-badge { background: #52c41a; }
.plan-name { font-size: 16px; font-weight: 700; color: #1e293b; margin-bottom: 2px; }
.plan-name-en { font-size: 12px; color: #94a3b8; margin-bottom: 8px; }
.plan-price { font-size: 28px; font-weight: 800; color: #2563eb; margin: 8px 0 2px; }
.plan-price.free { color: #64748b; font-size: 20px; }
.plan-price small { font-size: 14px; font-weight: 400; color: #64748b; }
.plan-original { font-size: 12px; color: #94a3b8; margin-bottom: 4px; }
.plan-discount { color: #52c41a; font-weight: 600; margin-left: 6px; }
.plan-credits { font-size: 13px; color: #64748b; margin-bottom: 12px; }
.plan-features { list-style: none; padding: 0; margin: 0 0 16px; }
.plan-features li { font-size: 13px; color: #475569; line-height: 2; }
.plan-features li::before { content: '✓ '; color: #52c41a; font-weight: 700; }
.plan-btn { width: 100%; }
.comparison-card { margin-bottom: 24px; }
.comparison-card :deep(td) { font-size: 13px; }
.package-card { margin-bottom: 24px; }
.package-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; }
.package-item {
border: 1px solid #e5e7eb; border-radius: 12px; padding: 16px; text-align: center;
transition: border-color 0.2s;
}
.package-item:hover { border-color: #2563eb; }
.pkg-name { font-size: 15px; font-weight: 600; color: #1e293b; }
.pkg-credits { font-size: 20px; font-weight: 700; color: #2563eb; margin: 6px 0; }
.pkg-credits small { font-size: 12px; font-weight: 400; }
.pkg-price { font-size: 18px; font-weight: 700; color: #e6a23c; margin-bottom: 2px; }
.pkg-unit { font-size: 11px; color: #94a3b8; margin-bottom: 10px; }
</style> </style>
@@ -74,6 +74,117 @@
</div> </div>
</section> </section>
<!-- Ecosystem Section -->
<section class="ecosystem">
<h2 class="section-title">TradeMate 产品生态</h2>
<p class="section-subtitle">一个账号三种方式使用网页端浏览器插件AI 编程技能包覆盖所有场景</p>
<div class="eco-grid">
<div class="eco-card">
<div class="eco-icon"><el-icon :size="32" color="#1890ff"><Monitor /></el-icon></div>
<h3>网页工作台</h3>
<p class="eco-desc">全功能外贸工作台翻译客户管理营销生成报价单AI 数字员工浏览器打开即用</p>
<ul class="eco-features">
<li>智能翻译 · 20+ 语言</li>
<li>CRM 客户管理 + 健康评分</li>
<li>AI 营销文案 + 报价单</li>
<li>AI 数字员工自动化</li>
</ul>
<div class="eco-badge">当前产品</div>
</div>
<div class="eco-card">
<div class="eco-icon"><el-icon :size="32" color="#faad14"><Chrome /></el-icon></div>
<h3>浏览器插件</h3>
<p class="eco-desc">Chrome 扩展可在任何网页上使用 TradeMate 功能划词翻译快速客户搜索营销生成</p>
<ul class="eco-features">
<li>右键划词翻译</li>
<li>一键搜索潜在客户</li>
<li>AI 回复建议生成</li>
<li>与网页工作台数据互通</li>
</ul>
<el-button size="small" type="warning" plain @click="showExtensionGuide = true">安装说明</el-button>
</div>
<div class="eco-card">
<div class="eco-icon"><el-icon :size="32" color="#722ed1"><Tools /></el-icon></div>
<h3>AI 技能包</h3>
<p class="eco-desc">开源 SKILL.md 技能包安装到 CursorClaude CodeOpenCode AI 编程工具中直接调用</p>
<ul class="eco-features">
<li>翻译 + 回复生成</li>
<li>客户发现 + 信息提取</li>
<li>营销内容生成</li>
<li>跨平台兼容Cursor / Claude / OpenCode</li>
</ul>
<el-button size="small" type="primary" plain @click="showSkillGuide = true">查看技能</el-button>
</div>
</div>
<!-- Extension Install Dialog -->
<el-dialog v-model="showExtensionGuide" title="TradeMate 浏览器插件" width="480px">
<div class="guide-content">
<h4>安装方式</h4>
<div class="guide-step">
<span class="step-num">1</span>
<span>下载项目中的 <code>browser-extension</code> 目录到本地</span>
</div>
<div class="guide-step">
<span class="step-num">2</span>
<span>打开 Chrome 浏览器进入 <code>chrome://extensions/</code></span>
</div>
<div class="guide-step">
<span class="step-num">3</span>
<span>开启"开发者模式"右上角开关</span>
</div>
<div class="guide-step">
<span class="step-num">4</span>
<span>点击"加载已解压的扩展程序"选择 <code>browser-extension</code> 目录</span>
</div>
<div class="guide-step">
<span class="step-num">5</span>
<span>点击浏览器工具栏的 TradeMate 图标输入 API 地址和登录凭据即可使用</span>
</div>
<el-divider />
<h4>功能预览</h4>
<div class="guide-preview">
<div><el-tag size="small">🌐 翻译</el-tag> 输入文本翻译右键划词翻译</div>
<div><el-tag size="small">💬 回复</el-tag> 根据询盘生成专业/友好的回复建议</div>
<div><el-tag size="small">🔍 发现</el-tag> Google 搜索潜在客户提取联系方式</div>
<div><el-tag size="small">📝 营销</el-tag> 生成产品营销文案和关键词</div>
</div>
<p class="guide-tip">注意需要先登录 TradeMate 工作台获取 Token</p>
</div>
</el-dialog>
<!-- Skills Dialog -->
<el-dialog v-model="showSkillGuide" title="AI 技能包 (SKILL.md)" width="480px">
<div class="guide-content">
<h4>可用的技能包</h4>
<el-table :data="skills" border stripe size="small">
<el-table-column prop="name" label="名称" width="140" />
<el-table-column prop="desc" label="功能" />
<el-table-column prop="api" label="调用 API" width="140" />
</el-table>
<el-divider />
<h4>安装方式</h4>
<div class="guide-step">
<span class="step-num">1</span>
<span>确认你使用的工具支持 SKILL.mdCursor / Claude Code / OpenCode </span>
</div>
<div class="guide-step">
<span class="step-num">2</span>
<span> <code>.opencode/skills/</code> 目录下的 <code>.md</code> 文件放入工具的技能目录</span>
</div>
<div class="guide-step">
<span class="step-num">3</span>
<span>在工具中通过关键词触发"翻译这段""找客户""生成营销文案"</span>
</div>
<div class="guide-step">
<span class="step-num">4</span>
<span>需要配置 <code>TRADEMATE_API_URL</code> <code>TRADEMATE_API_KEY</code></span>
</div>
<p class="guide-tip">技能包不消耗额外费用使用你的 TradeMate 账号积分</p>
</div>
</el-dialog>
</section>
<footer class="landing-footer"> <footer class="landing-footer">
<div class="footer-inner"> <div class="footer-inner">
<div class="footer-top"> <div class="footer-top">
@@ -209,6 +320,14 @@ function handleClick(f) {
} }
} }
const showExtensionGuide = ref(false)
const showSkillGuide = ref(false)
const skills = [
{ name: 'translate-reply', desc: 'AI 翻译 + 智能回复生成', api: '/translate, /translate/reply' },
{ name: 'customer-discovery', desc: 'Google 搜索客户 + 信息提取', api: '/discovery/search' },
{ name: 'marketing-content', desc: '营销文案/关键词生成', api: '/marketing/generate' },
]
function goWorkspace() { router.push('/workspace') } function goWorkspace() { router.push('/workspace') }
</script> </script>
@@ -260,7 +379,37 @@ function goWorkspace() { router.push('/workspace') }
.gongan-link { display: inline-flex; align-items: center; gap: 4px; } .gongan-link { display: inline-flex; align-items: center; gap: 4px; }
.gongan-icon { height: 16px; vertical-align: middle; } .gongan-icon { height: 16px; vertical-align: middle; }
/* Ecosystem */
.ecosystem { max-width: 1200px; margin: 0 auto 40px; padding: 40px 20px 0; text-align: center; }
.section-title { font-size: 26px; color: #1e293b; margin-bottom: 8px; position: relative; display: inline-block; }
.section-title::after { content: ''; display: block; width: 40px; height: 3px; background: #1890ff; margin: 10px auto 0; border-radius: 2px; }
.section-subtitle { color: #64748b; font-size: 14px; margin-bottom: 36px; }
.eco-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
.eco-card {
background: #fff; border-radius: 16px; padding: 32px 24px; text-align: left;
box-shadow: 0 2px 12px rgba(0,0,0,0.06); transition: all 0.25s; position: relative; display: flex; flex-direction: column;
}
.eco-card:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(0,0,0,0.1); }
.eco-icon { width: 56px; height: 56px; background: #f8faff; border-radius: 14px; display: flex; align-items: center; justify-content: center; margin-bottom: 16px; }
.eco-card h3 { font-size: 18px; color: #1e293b; margin-bottom: 8px; }
.eco-desc { font-size: 13px; color: #64748b; line-height: 1.6; margin-bottom: 16px; flex: 1; }
.eco-features { list-style: none; padding: 0; margin: 0 0 20px; }
.eco-features li { font-size: 13px; color: #475569; line-height: 2; padding-left: 20px; position: relative; }
.eco-features li::before { content: '✓'; position: absolute; left: 0; color: #52c41a; font-weight: 700; }
.eco-badge { position: absolute; top: 12px; right: 12px; background: #e6f7ff; color: #1890ff; font-size: 11px; padding: 2px 10px; border-radius: 10px; font-weight: 600; }
/* Guide Dialogs */
.guide-content { font-size: 14px; color: #333; }
.guide-content h4 { font-size: 15px; color: #1e293b; margin-bottom: 12px; }
.guide-step { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 12px; font-size: 13px; line-height: 1.5; }
.step-num { flex-shrink: 0; width: 22px; height: 22px; background: #1890ff; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; }
.guide-content code { background: #f0f4ff; color: #1890ff; padding: 1px 6px; border-radius: 4px; font-size: 12px; }
.guide-preview { display: flex; flex-direction: column; gap: 8px; }
.guide-preview div { font-size: 13px; color: #475569; display: flex; align-items: center; gap: 8px; }
.guide-tip { margin-top: 16px; font-size: 12px; color: #94a3b8; background: #f8fafc; padding: 10px 14px; border-radius: 8px; }
@media (max-width: 768px) { @media (max-width: 768px) {
.eco-grid { grid-template-columns: 1fr; gap: 16px; }
.hero-inner { flex-direction: column; padding: 40px 20px; } .hero-inner { flex-direction: column; padding: 40px 20px; }
.hero-right { width: 100%; } .hero-right { width: 100%; }
.feature-grid { grid-template-columns: repeat(2, 1fr); } .feature-grid { grid-template-columns: repeat(2, 1fr); }