feat: restructure pricing models, add upgrade modal and ecosystem UI
This commit is contained in:
@@ -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">×</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>
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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: '数据分析' } },
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
|||||||
+268
-100
@@ -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">
|
</el-radio-group>
|
||||||
¥{{ 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>
|
</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>
|
||||||
<div style="text-align:center;margin-top:16px">
|
|
||||||
<el-button v-if="p.id === currentPlan" type="default" disabled>当前套餐</el-button>
|
<div class="plans-grid" v-loading="loading">
|
||||||
<el-button v-else-if="p.id === 'free'" @click="handleFree">当前套餐</el-button>
|
<!-- Free Tier -->
|
||||||
<el-button v-else type="primary" :loading="loadingId === p.id" @click="showPayDialog(p.id)">{{ p.price === 0 ? '当前套餐' : '升级' }}</el-button>
|
<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>
|
||||||
</el-card>
|
</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">
|
<!-- Purchase Dialog -->
|
||||||
<div style="text-align:center;padding:20px 0" v-if="!payDialog.orderCreated">
|
<el-dialog v-model="payDialog.visible" title="选择支付方式" width="360px">
|
||||||
<el-radio-group v-model="payDialog.payType" style="margin-bottom:24px">
|
<p style="margin-bottom:12px;text-align:center" v-if="payDialog.type === 'subscription'">
|
||||||
<el-radio-button value="alipay">
|
订阅 <strong>{{ payDialog.plan?.name }}</strong>
|
||||||
<span style="display:flex;align-items:center;gap:6px;padding:0 20px">
|
({{ payDialog.plan?.credits }} 积分/月)
|
||||||
<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>
|
</p>
|
||||||
支付宝
|
<p style="margin-bottom:12px;text-align:center" v-else>
|
||||||
</span>
|
购买 <strong>{{ payDialog.pkg?.name }}</strong> ({{ payDialog.pkg?.credits }} 积分)
|
||||||
</el-radio-button>
|
</p>
|
||||||
<el-radio-button value="wechat">
|
<p style="font-size:22px;font-weight:bold;color:#e6a23c;text-align:center;margin-bottom:16px">
|
||||||
<span style="display:flex;align-items:center;gap:6px;padding:0 20px">
|
¥{{ payDialog.type === 'subscription' ? payDialog.plan?.price : payDialog.pkg?.price }}
|
||||||
<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>
|
</p>
|
||||||
微信支付
|
<el-radio-group v-model="payDialog.payType" style="display:flex;gap:16px;justify-content:center;margin-bottom:16px">
|
||||||
</span>
|
<el-radio-button value="alipay">支付宝</el-radio-button>
|
||||||
</el-radio-button>
|
<el-radio-button value="wechat">微信支付</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
<div>
|
<template #footer>
|
||||||
<el-button type="primary" size="large" :loading="payDialog.loading" @click="handleUpgrade">立即支付</el-button>
|
<el-button @click="payDialog.visible = false">取消</el-button>
|
||||||
</div>
|
<el-button type="primary" @click="confirmPay" :loading="payDialog.loading">确认支付</el-button>
|
||||||
</div>
|
</template>
|
||||||
<div style="text-align:center;padding:20px 0" v-else>
|
|
||||||
<div v-if="payDialog.codeUrl">
|
|
||||||
<p style="margin-bottom:16px;color:#666">请使用微信扫描下方二维码支付</p>
|
|
||||||
<img :src="payDialog.codeUrl" style="width:200px;height:200px;border:1px solid #eee;border-radius:8px" />
|
|
||||||
<p style="margin-top:12px;font-size:12px;color:#999">支付成功后自动生效</p>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="payDialog.payUrl">
|
|
||||||
<p style="margin-bottom:16px;color:#666">正在跳转支付宝...</p>
|
|
||||||
<el-button type="primary" @click="openPayUrl">前往支付</el-button>
|
|
||||||
</div>
|
|
||||||
<el-button style="margin-top:16px" @click="payDialog.visible = false">关闭</el-button>
|
|
||||||
</div>
|
|
||||||
</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 = {
|
||||||
|
starter: { badge: '入门', yearlyDiscount: 0.17, features: ['200 积分/月', '无每日限制', '翻译 + 客户发现 + 营销生成', '智能回复 + 报价单'] },
|
||||||
|
pro: { badge: '推荐', featured: true, yearlyDiscount: 0.15, features: ['1000 积分/月', 'AI 数字员工', '团队协作(3 人)', '优先技术支持'] },
|
||||||
|
enterprise: { badge: '旗舰', yearlyDiscount: 0.16, features: ['2500 积分/月', '不限团队人数', 'API 调用权限', 'SLA 保障'] },
|
||||||
|
}
|
||||||
|
|
||||||
|
const planCards = computed(() => {
|
||||||
|
return plans.value.map(p => {
|
||||||
|
const meta = PLAN_META[p.id] || {}
|
||||||
|
const yearlyOriginal = Math.round(p.price * 12)
|
||||||
|
const yearlyPrice = Math.round(p.price * 12 * (1 - (meta.yearlyDiscount || 0)))
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
credits: p.credits_per_month || p.credits || 0,
|
||||||
|
badge: meta.badge || '',
|
||||||
|
featured: meta.featured || false,
|
||||||
|
isCurrent: p.id === currentPlan.value,
|
||||||
|
yearlyPrice,
|
||||||
|
yearlyOriginal,
|
||||||
|
discountPct: Math.round((meta.yearlyDiscount || 0) * 100),
|
||||||
|
features: meta.features || [],
|
||||||
|
}
|
||||||
|
}).filter(p => p.price > 0) // exclude free
|
||||||
|
})
|
||||||
|
|
||||||
|
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 {
|
try {
|
||||||
const [plansRes, subRes] = await Promise.all([getPlans(), getSubscription().catch(() => null)])
|
const [plansRes, pkgsRes, balanceRes] = await Promise.all([
|
||||||
const pd = plansRes.data || plansRes
|
getSubscriptionPlans().catch(() => []),
|
||||||
plans.value = (pd.plans || pd.items || pd || []).filter(p => p.id !== 'free')
|
getCreditPackages().catch(() => []),
|
||||||
if (subRes) {
|
getCreditBalance().catch(() => null),
|
||||||
const sd = subRes.data || subRes
|
])
|
||||||
currentPlan.value = sd.plan_id || sd.plan
|
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 */ }
|
} catch { /* ignore */ }
|
||||||
})
|
loading.value = false
|
||||||
|
|
||||||
function showPayDialog(planId) {
|
|
||||||
payDialog.planId = planId
|
|
||||||
payDialog.payType = 'alipay'
|
|
||||||
payDialog.orderCreated = false
|
|
||||||
payDialog.payUrl = ''
|
|
||||||
payDialog.codeUrl = ''
|
|
||||||
payDialog.visible = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUpgrade() {
|
async function handleUpgrade(plan) {
|
||||||
payDialog.loading = true
|
if (plan.isCurrent) return
|
||||||
|
payDialog.value = {
|
||||||
|
visible: true,
|
||||||
|
type: 'subscription',
|
||||||
|
plan,
|
||||||
|
pkg: null,
|
||||||
|
payType: 'alipay',
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmPay() {
|
||||||
|
const d = payDialog.value
|
||||||
|
d.loading = true
|
||||||
try {
|
try {
|
||||||
const res = await createOrder(payDialog.planId, payDialog.payType)
|
if (d.type === 'subscription') {
|
||||||
payDialog.orderCreated = true
|
const res = await subscribeCreditPlan(d.plan.id, d.payType)
|
||||||
if (res.code_url) {
|
if (res.pay_url) window.open(res.pay_url, '_blank')
|
||||||
payDialog.codeUrl = res.code_url
|
else ElMessage.success('订阅成功!')
|
||||||
} else if (res.pay_url) {
|
|
||||||
payDialog.payUrl = res.pay_url
|
|
||||||
window.open(res.pay_url)
|
|
||||||
} else {
|
} else {
|
||||||
ElMessage.success('订单已创建,请稍后查看')
|
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) {
|
} catch (e) {
|
||||||
ElMessage.error(e?.detail || '下单失败')
|
ElMessage.error(e?.detail || e?.message || '支付失败')
|
||||||
} finally {
|
}
|
||||||
payDialog.loading = false
|
d.loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function buyPackage(pkg) {
|
||||||
|
payDialog.value = {
|
||||||
|
visible: true,
|
||||||
|
type: 'package',
|
||||||
|
plan: null,
|
||||||
|
pkg,
|
||||||
|
payType: 'alipay',
|
||||||
|
loading: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPayUrl() {
|
onMounted(loadData)
|
||||||
if (payDialog.payUrl) window.open(payDialog.payUrl)
|
|
||||||
}
|
|
||||||
</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 技能包,安装到 Cursor、Claude Code、OpenCode 等 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.md(Cursor / 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); }
|
||||||
|
|||||||
Reference in New Issue
Block a user