refactor: switch to free-trial/private-deploy/buyout pricing, fix feature gaps, add SEO landing + deploy config

This commit is contained in:
TradeMate Dev
2026-07-12 08:09:28 +08:00
parent 9ca5d79d8a
commit 04924e3bc4
36 changed files with 1026 additions and 975 deletions
-85
View File
@@ -1,85 +0,0 @@
# TradeMate Customer Discovery Skill
Search and discover potential foreign trade customers from the web.
## Description
Automatically searches Google for potential buyers in your target market, extracts company information and contact details (email, phone, WhatsApp), and scores leads by relevance. Helps foreign trade professionals find new customers without manual browsing.
## Triggers
- "find customers" / "找客户"
- "discover leads" / "发现潜在客户"
- "search buyers" / "搜索买家"
- "customer discovery" / "客户发现"
## Workflow
### 1. Search for Potential Customers
```
POST /api/v1/discovery/search
Content-Type: application/json
Authorization: Bearer <token>
{
"keyword": "<product or industry keyword>",
"market": "US|UK|DE|FR|AU|...", // target country code
"max_results": 10
}
```
Response:
```json
{
"results": [
{
"title": "Company Name",
"url": "https://company-website.com",
"description": "...",
"emails": ["info@company.com"],
"phones": ["+1-xxx-xxx-xxxx"],
"social": {
"whatsapp": "...",
"wechat": "..."
},
"relevance_score": 85
}
],
"total": 10
}
```
### 2. Save High-Scoring Leads
High-scoring leads (score >= 70) are automatically saved as Customer records in the TradeMate CRM via the Agent pipeline:
```
POST /api/v1/agent/start
Content-Type: application/json
Authorization: Bearer <token>
{
"product_name": "<your product>",
"product_description": "<product description>",
"target_market": "<target country>"
}
```
## Configuration
Requires a running TradeMate backend instance with:
- Google Custom Search API configured
- Valid API token with discovery credits
```bash
export TRADEMATE_API_URL=http://localhost:8000
export TRADEMATE_API_KEY=<your-api-token>
```
## Notes
- Each search consumes discovery credits (check balance via `GET /api/v1/credits/balance`)
- Free tier: limited searches per day
- Pro tier: expanded daily quota
- Enterprise tier: unlimited searches + auto-save to CRM
-106
View File
@@ -1,106 +0,0 @@
# TradeMate Marketing Content Skill
Generate marketing copy and keyword suggestions for foreign trade products.
## Description
AI-powered marketing content generation for export products. Creates professional marketing copy in multiple languages and styles, generates SEO keywords, and analyzes competitor positioning. Designed for foreign trade professionals who need compelling product descriptions for international buyers.
## Triggers
- "generate marketing" / "生成营销文案"
- "write product description" / "写产品描述"
- "marketing keywords" / "营销关键词"
- "product copy" / "产品文案"
## Workflow
### 1. Generate Marketing Copy
```
POST /api/v1/marketing/generate
Content-Type: application/json
Authorization: Bearer <token>
{
"product_name": "<product name>",
"description": "<product description>",
"category": "<category>",
"target": "<target market, e.g. US importers>",
"style": "professional|friendly|luxury",
"count": 3,
"language": "en|zh"
}
```
Response:
```json
{
"results": [
{
"content": "Professional marketing copy...",
"style": "professional",
"provider": "sensenova"
}
],
"product": "...",
"target": "...",
"count": 3,
"credits_remaining": 45
}
```
### 2. Generate Keywords
```
POST /api/v1/marketing/keywords
Content-Type: application/json
Authorization: Bearer <token>
{
"product_name": "<product name>",
"description": "<product description>",
"category": "<category>",
"language": "en",
"count": 10
}
```
Response:
```json
{
"keywords": ["keyword1", "keyword2", ...],
"product": "...",
"credits_remaining": 45
}
```
### 3. Competitor Analysis
```
POST /api/v1/marketing/competitor-analysis
Content-Type: application/json
Authorization: Bearer <token>
{
"product_name": "<product name>",
"description": "<product description>",
"competitors": ["competitor1", "competitor2"]
}
```
## Configuration
Requires a running TradeMate backend instance:
```bash
export TRADEMATE_API_URL=http://localhost:8000
export TRADEMATE_API_KEY=<your-api-token>
```
## Notes
- Each generation consumes 5 marketing credits
- Free tier: limited daily generations
- Multiple AI providers (Sensenova / NVIDIA) with automatic fallback
- Supports English and Chinese output
-97
View File
@@ -1,97 +0,0 @@
# TradeMate Translate & Reply Skill
Translate foreign trade inquiries and generate professional replies.
## Description
AI-powered translation and smart reply generation for foreign trade professionals. Supports Chinese-English bidirectional translation with trade-specific context, and generates reply suggestions in multiple tones (professional/friendly).
## Triggers
- "translate this" / "翻译"
- "reply to inquiry" / "回复询盘"
- "generate reply" / "生成回复"
- "translate for trade" / "外贸翻译"
## Workflow
### 1. Translate Text
```
POST /api/v1/translate
Content-Type: application/json
Authorization: Bearer <token>
{
"text": "<text to translate>",
"target_lang": "zh|en",
"context": "trade" // optional, adds trade-specific context
}
```
Response:
```json
{
"translated_text": "...",
"source_lang": "en",
"provider_used": "sensenova",
"from_cache": false
}
```
### 2. Generate Reply
```
POST /api/v1/translate/reply
Content-Type: application/json
Authorization: Bearer <token>
{
"inquiry": "<customer inquiry text>",
"tone": "professional|friendly",
"count": 2,
"context": {
"product": "<optional product name>",
"price": "<optional price info>"
}
}
```
Response:
```json
{
"suggestions": [
{"reply": "...", "tone": "professional", "provider": "sensenova"},
{"reply": "...", "tone": "friendly", "provider": "sensenova"}
],
"count": 2
}
```
### 3. Extract Info from Inquiry
```
POST /api/v1/translate/extract
Content-Type: application/json
Authorization: Bearer <token>
{
"text": "<inquiry text>",
"extract_type": "inquiry"
}
```
## Configuration
Requires a running TradeMate backend instance. Set environment variables:
```bash
export TRADEMATE_API_URL=http://localhost:8000
export TRADEMATE_API_KEY=<your-api-token>
```
## Notes
- Free tier: limited daily translations
- Pro tier: unlimited usage
- Uses Sensenova (商汤) as default AI provider, falls back to NVIDIA
+29 -4
View File
@@ -1,5 +1,20 @@
# TradeMate (外贸小助手) — Agent Guide
## Chrome 浏览器扩展 🆕
- **目录**: `browser-extension/` — 完整 Chrome 插件
- **功能**: popup 面板调用 AI 翻译/回复/提取客户信息、网页右键菜单集成、自动填写报价单
- **兼容平台**: LinkedIn / Amazon / AliExpress 等外贸平台页面
- **安装**: 打开 `chrome://extensions` → 加载已解压的扩展 → 选择 `browser-extension/` 目录
- **API 客户端**: `browser-extension/api/client.js` — 封装后端 API 调用,支持 JWT 认证
## UX Design
- **PC 工作台**: 侧边栏 5 项精简导航 (首页/客户/业务/翻译/更多) + **CommandK 命令面板** (Ctrl+K)
- **移动端 H5**: 底部 4 Tab 导航 (首页/客户/业务/我的)
- **工作区合并**: Customers/Discovery/Followup → `WorkspaceCustomer`Translate/Products/Quotations/Marketing → `WorkspaceBiz`
- **路由**: 旧页面保留隐藏路由,保证兼容性
## uni-app 移动端 H5 (改版后)
- **Tab Bar**: 4 项底部导航 — 首页 / 客户 / 业务 / 我的
@@ -36,9 +51,16 @@
- `GET /api/v1/agent/pipelines` — 任务列表
- `GET /api/v1/agent/{pipeline_id}` — 任务详情
- **流程**: 用户输入产品+市场 → AgentOrchestrator 串接 DiscoveryService.search() → analyze() → outreach() → 自动保存高匹配客户
- **前端入口**: `UserLayout.vue` 侧边栏首位 "AI数字员工" (MagicStick 图标)
- **前端入口**: `user-frontend/src/views/Agent.vue` 已接入路由 `/workspace/agent``UserLayout.vue` 侧边栏 "AI数字员工" (MagicStick 图标)`/agent` 重定向到 `/workspace/agent`
- **迁移**: 需要运行 `alembic revision --autogenerate -m "add agent_pipelines"` 创建 `agent_pipelines`
## 收费方式(免费试用 / 私有化部署 / 买断源码)
- **页面**: `user-frontend/src/views/Upgrade.vue` + `UpgradeModal.vue``uni-app/src/pages/upgrade/upgrade.vue`、浏览器插件升级入口均改为三种方案:免费试用 / 私有化部署(年付授权)/ 买断源码(一次性)
- **企业线索**: 私有化部署与买断源码的 CTA 提交到 `POST /api/v1/leads`(模型 `EnterpriseLead`,表 `enterprise_leads`),无需登录;admin 可在 `enterprise_leads` 表查看
- **价格占位**: `Upgrade.vue``PRICING` 对象(¥39,800/年、¥98,000 一次性)为可配置展示值
- **说明**: 旧的按月订阅套餐(`/credits/subscribe`)仍存在于后端,但前端不再主推;如彻底下线订阅需同步清理 `credits.py` 订阅逻辑
## Architecture
- **Backend**: `backend/` — FastAPI + SQLAlchemy 1.4 async + asyncpg, single `app.main:app`
@@ -50,8 +72,8 @@
## AI Providers
- **Active**: Sensenova (商汤), NVIDIA, 阿里机器翻译 (alibaba-mt) — 5 providers in DB
- **Removed**: Claude, DeepL, Local, OpencodeGo, 讯飞 Spark — all git rm'd
- **Active (seeded by `seed_from_env()`)**: Sensenova (商汤, if `SENSENOVA_API_KEY` set), NVIDIA (if `NVIDIA_API_KEY` set), 阿里翻译 alibaba-mt (always inserted) — at most 3 rows. Do NOT assume 5 providers exist.
- **Removed**: Claude, DeepL, Local, OpencodeGo, 讯飞 Spark — provider implementations were git rm'd; don't re-add them.
- **DB-driven**: `AIProvider` model + `admin_ai.py` API — manage providers at runtime. `router.seed_from_env()` loads from `.env` on startup
- **ECS RAM role**: 阿里翻译使用 ECS 实例 RAM 角色 `trademate-translate` 获取 STS 临时凭证
- **Provider type mapping** in `router.py._build_provider()`: sensenova, nvidia, alibaba-mt
@@ -90,6 +112,8 @@ cd user-frontend && npm run dev # port 5174, base: /workspace/
cd backend && venv/bin/pytest # all
venv/bin/pytest tests/test_auth_api.py # single file
venv/bin/pytest tests/ -k "test_login" # keyword filter
# pytest.ini: asyncio_mode=auto (no @pytest.mark.asyncio needed) and
# addopts adds --cov=app coverage automatically on every run.
# Builds
cd uni-app && npm run build:h5 # uni-app (mobile H5)
@@ -110,6 +134,7 @@ alembic revision --autogenerate -m "desc"
- **Nginx**: SPA fallbacks for `/app/`, `/admin/`, `/workspace/`
- **vite config**: each project has its own `base` path and dev port
- **API**: proxied via nginx `location /api/` to `127.0.0.1:8000`
- **Makefile `make deploy`** references `docker-compose.prod.yml` / `docker-compose.staging.yml`, which do **not** exist (only `docker-compose.yml` is present). Those targets are currently broken.
## Critical Quirks
@@ -132,7 +157,7 @@ alembic revision --autogenerate -m "desc"
## Project Conventions
- **No README.md** — key context is in `PROGRESS.md` and `docs/`
- **Docs**: `README.md` (overview), `PROGRESS.md` (task status), `docs/` (API/schema/architecture). README references this file for dev norms.
- **Chinese UI** — mobile-first, for foreign-trade SOHOs/small teams
- **No comments in code** unless explicitly asked
- **Commit messages** focus on "why" not "what", in English
+111 -8
View File
@@ -1,7 +1,7 @@
# TradeMate (外贸小助手) - 项目进度文档
**更新时间**: 2026-06-16 18:30
**状态**: ✅ 生产环境运行中 — AI 路由 DB 驱动 + 翻译配额全链路 + ECS RAM 角色认证 + AI 数字员工
**更新时间**: 2026-07-11
**状态**: ✅ 生产环境运行中 — 收费方式调整(免费试用/私有化部署/买断源码)+ 功能审计补全 + SEO 营销落地页 + 部署优化
---
@@ -82,7 +82,7 @@
| **编排服务** | `services/agent_orchestrator.py` | 串接 DiscoveryService → 分析 → 评分 → 触达 → 自动入库 |
| **Agent API** | `api/v1/agent.py` | 3 端点: POST /start, GET /pipelines, GET /{id} |
| **Agent 仪表盘** | `user-frontend/src/views/Agent.vue` | 统计卡片 + 任务列表 + 流水线进度 + 线索表格 + 触达预览 |
| **侧边栏入口** | `layouts/UserLayout.vue` | "AI数字员工" 作为首位菜单项,图标 MagicStick |
| **侧边栏入口** | `layouts/UserLayout.vue` | "AI数字员工" 已接入路由 `/workspace/agent` 与侧边栏菜单(图标 MagicStick);`/agent` 重定向到 `/workspace/agent` |
**工作流程**:
1. 用户输入产品名称 + 描述 + 目标市场
@@ -199,7 +199,69 @@
| Docker Compose 增强 | 添加 nginx/admin/user/uni-app 服务 + 独立网络 + Redis AOF |
| CSRF 保护 | 双提交 Cookie 模式,auth/payment/profile 必检 |
### 14. 核心 API 测试通过
### 14. PC 工作台 UX 大幅重构 — 合并工作区 + CommandK 命令面板 ✅ [NEW]
| 功能 | 说明 |
|------|------|
| **侧边栏精简** | 从 11 项合并为 5 项(首页/客户/业务/翻译/更多),减少 4 层级为 5 个清晰入口 |
| **CommandK 命令面板** | `user-frontend/src/components/CommandK.vue` — 全局 Ctrl+K 调出,快速搜索/导航/操作 |
| **工作区合并** | 客户/挖掘/跟进 → `WorkspaceCustomer.vue`;翻译/产品/报价/营销 → `WorkspaceBiz.vue` |
| **仪表盘简化** | `NewHome.vue` 精简为数据看板,去掉冗余入口 |
| **旧路由保留** | 旧页面可通过隐藏路由访问,保证兼容性 |
### 15. uni-app 移动端 4Tab 合并重构 ✅ [NEW]
| 功能 | 说明 |
|------|------|
| **Tab Bar 精简** | 从 5 项(首页/客户/营销/报价/我的)改为 4 项(首页/客户/业务/我的) |
| **客户工作台** | `workspace-customer.vue` — 3 个内联标签(客户列表/挖掘新客/智能跟进) |
| **业务工作台** | `workspace-biz.vue` — 4 个内联标签(翻译/产品库/报价单/营销素材) |
| **个人中心更新** | `profile.vue` — 通知中心/数据分析/团队/升级会员/联系客服 |
| **pages.json** | 标签栏配置同步更新,新增 workspace 页面注册 |
### 16. 定价模型重构 + 升级弹窗 + 生态 UI ✅ [NEW]
| 功能 | 说明 |
|------|------|
| **定价页重构** | `Upgrade.vue` 重新设计,新增 Yearly 套餐卡片 + 功能对比表 |
| **升级弹窗** | `UpgradeModal.vue` — 全局组件,任何页面可直接调起 |
| **生态入口** | `WorkspaceLanding.vue` — 工作台首页展示套餐状态 + 升级入口 |
| **路由整合** | 新增 `/workspace/upgrade` 路由,侧边栏可直接访问 |
### 17. 产品策略文档 + Agent 技能 + Chrome 浏览器扩展 ✅ [NEW]
| 功能 | 说明 |
|------|------|
| **产品策略文档** | `docs/PRODUCT_STRATEGY.md` — 319 行完整产品路线图(市场分析/定位/功能规划/定价策略/增长路线) |
| **Agent 技能定义** | 3 个 `.opencode/skills/` 文件 — `customer-discovery.md` / `marketing-content.md` / `translate-reply.md` |
| **Chrome 扩展** | `browser-extension/` — 完整 Chrome 插件,支持 LinkedIn/Amazon/AliExpress 页面集成 |
| **浏览器扩展功能** | popup 面板调用 API 翻译/提取客户信息、网页右键菜单、自动填写报价单 |
### 18. 管理后台 AI 路由增强 ✅ [NEW]
| 功能 | 说明 |
|------|------|
| **新增路由标签** | agent (数字员工)、followup (智能跟进)、outreach (开发信)、competitor (竞品分析) |
| **一行代码改动** | `admin-frontend/src/views/Config.vue` 更新 `ai_routing` 字段标签 |
### 19. 用户工作台 i18n 导航键更新 ✅ [NEW]
| 功能 | 说明 |
|------|------|
| **中文 locale** | `zh-CN.json` — 新增 home/customers/biz/agent 导航键 |
| **英文 locale** | `en.json` — 同步新增对应导航键 |
| **双语言适配** | 确保重构后的侧边栏/面包屑/页面标题完整支持中英文切换 |
### 20. 后端持续优化 ✅ [NEW]
| 序号 | 文件 | 问题描述 | 状态 |
|------|------|----------|------|
| 16 | `customer_health.py` | 健康分计算逻辑优化,增加活跃度权重 | ✅ 已修复 |
| 17 | `corpus_trainer.py` | 语料库训练流程修复,避免空数据处理 | ✅ 已修复 |
| 18 | `middleware.py` | 中间件性能优化,减少请求路径匹配耗时 | ✅ 已修复 |
| 19 | `conftest.py` | 测试基础设施增强,新增 fixture 复用 | ✅ 已修复 |
### 21. 核心 API 测试通过
| 功能 | 接口 | 状态 |
|------|------|------|
@@ -222,6 +284,39 @@
---
### 22. 收费方式调整 + 功能审计补全 + SEO/营销落地页 + 部署优化 ✅ [NEW]
> **方向调整(2026-07-11)**:停止以「按月订阅」为主推的 SaaS 收费模型,改为合规的「免费试用 / 私有化部署 / 买断源码」三档方案。
> 合规依据:仅持 ICP 备案即可销售「免费试用 + 私有化部署(年付授权)+ 买断源码(一次性软件/服务合同)」;若上线「托管付费订阅」需另行办理 ICP 经营许可证。
| 模块 | 文件 | 说明 |
|------|------|------|
| **定价页重构** | `user-frontend/src/views/Upgrade.vue` + `UpgradeModal.vue``uni-app/src/pages/upgrade/upgrade.vue` | 三档方案:免费试用 ¥0 / 私有化部署 ¥39,800 年 / 买断源码 ¥98,000 一次性;价格为可配置展示值 |
| **企业线索后端** | `backend/app/api/v1/leads.py` + `models/enterprise_lead.py` + `alembic/versions/add_enterprise_leads.py` | 新增 `POST /api/v1/leads`(免登录),`EnterpriseLead` 模型 + `enterprise_leads` 表;CSRF 对 `/api/v1/leads` 放行 |
| **线索测试** | `backend/tests/test_leads_api.py` | 3 个用例全部通过 |
| **功能审计补全** | `user-frontend/src/router/index.js``UserLayout.vue``admin-frontend/src/api/index.js``admin-frontend/src/views/Users.vue` | 修复 Agent 路由未接入工作台;修复后台用户编辑调用错误端点(`PATCH /users/{id}/tier``POST .../toggle-active``PATCH .../role``/admin/usage-stats` |
| **浏览器扩展补全** | `browser-extension/api/client.js``popup/popup.js``popup/index.html` | 新增「提取客户信息」(`extractInfo()`)+ 「客户」Tab + 升级链接指向 `/workspace/upgrade` |
| **后端测试** | `backend` pytest | 158 用例 + 3 leads 用例全部通过,无失败 |
| **SEO 营销落地页** | `landing/index.html``landing/robots.txt``landing/sitemap.xml` | 语义化可抓取 HTML + JSON-LD`SoftwareApplication` + `FAQPage`+ OG/Twitter + 规范化链接 + 内链到 `/app/``/workspace/`、定价;`robots.txt`/`sitemap.xml` 置于站点根 |
| **前端 SEO 增强** | `uni-app/index.html` | 修复损坏的 `og:image`/`twitter:image`(指向 `/app/static/images/yzr/yuzhiran.jpg`),更新描述为新三档定价 |
| **部署配置** | `deploy/frontend/nginx.conf` | 四路布局:`/` → 营销落地页(根)、`/app/` → uni-app H5、`/workspace/` → 用户工作台、`/admin/` → 管理后台、`/api/` → 后端 127.0.0.1:8000;各 SPA 带 `try_files ... /index.html` fallback |
**部署目录结构(目标)**
```
/www/wwwroot/trade.yuzhiran.com/
├── index.html ← landing/ 营销落地页(SEO 主入口)
├── robots.txt ← landing/robots.txt
├── sitemap.xml ← landing/sitemap.xml
├── app/ ← uni-app 构建产物 (base: /app/)
├── workspace/ ← user-frontend 构建产物 (base: /workspace/)
├── admin/ ← admin-frontend 构建产物 (base: /admin/)
└── (后端由 systemd/uvicorn 运行,经 /api/ 代理)
```
**注意**:本环境无服务器 SSH 凭据,无法实际推送部署;上述产物与 `nginx.conf` 为部署就绪状态,需在目标服务器执行构建 + 拷贝 + `alembic upgrade head` + 重载 nginx。
---
## 三、待办事项
### 低优先级
@@ -369,16 +464,23 @@ trade.yuzhiran.com/
### 8.3 部署流程
```bash
# 前端构建 & 部署
# 1. 营销落地页(SEO 主入口)
cp landing/index.html /www/wwwroot/trade.yuzhiran.com/index.html
cp landing/robots.txt landing/sitemap.xml /www/wwwroot/trade.yuzhiran.com/
# 2. 三端 SPA 构建 & 部署
cd uni-app && npm run build:h5 && cp -r dist/build/h5/* /www/wwwroot/trade.yuzhiran.com/app/
cd user-frontend && npm run build && cp -r dist/* /www/wwwroot/trade.yuzhiran.com/workspace/
cd admin-frontend && npm run build && cp -r dist/* /www/wwwroot/trade.yuzhiran.com/admin/
# 后端重启 (systemd)
# 3. 后端:迁移 + 重启 (systemd)
cd backend && source venv/bin/activate && alembic upgrade head
sudo systemctl restart ftrade-backend.service
# 查看启动日志
sudo journalctl -u ftrade-backend.service -n 20
# 4. Nginx:套用 deploy/frontend/nginx.conf 的 4 路布局后重载
sudo nginx -t && sudo nginx -s reload
# 本地开发启动
cd backend && source venv/bin/activate && uvicorn app.main:app --reload --port 8000
```
@@ -389,6 +491,7 @@ cd backend && source venv/bin/activate && uvicorn app.main:app --reload --port 8
| 日期 | 变更内容 |
|------|----------|
| 2026-07-11 | **收费方式调整**: 免费试用/私有化部署/买断源码三档;功能审计补全;SEO 营销落地页 + nginx 四路布局部署优化 |
| 2026-06-16 | **AI 数字员工**: AgentOrchestrator 编排服务 + AgentPipeline 模型 + Agent API + 前端仪表盘 |
| 2026-06-02 | 生产环境部署 + AI 路由 DB 驱动 + 翻译配额扩展至 LLM + ECS RAM 角色认证 + 删除 OpencodeGo/Spark |
| 2026-05-29 | 安全加固 (T-005): 限流/CSRF/CORS + AI 提供商 DB 管理 + 客户挖掘联系人提取 |
+52 -6
View File
@@ -32,7 +32,19 @@ TradeMate 提供三种使用方式,共享同一后端和账号体系:
| 🧩 **Chrome 浏览器插件** | 划词翻译、快捷客户搜索、营销生成 | 见 `browser-extension/` [安装指南](browser-extension/INSTALL.html) |
| 🤖 **AI 技能包** | SKILL.md 技能包,用于 Cursor/Claude Code/OpenCode | `.opencode/skills/` |
> 三者独立运营、互相补充。订阅一个入口,全平台可用
> 三者独立运营、互相补充,共享同一后端和账号体系
## 💰 收费方式
采用合规的「软件 + 服务」销售模式,不主推托管订阅:
| 方案 | 价格 | 说明 |
|------|------|------|
| 🆓 **免费试用** | ¥0 | 注册即享核心功能体验 |
| 🏢 **私有化部署** | ¥39,800 / 年 | 年付授权,独立部署到客户自有服务器 |
| 💎 **买断源码** | ¥98,000 / 一次性 | 一次性买断完整前后端源码,可二次开发 |
> 企业线索(私有化/买断意向)通过 `POST /api/v1/leads` 提交至 `enterprise_leads` 表,由后台跟进。旧版按月订阅后端逻辑保留但前端不再主推。
---
@@ -153,12 +165,12 @@ npm run dev
| `SECRET_KEY` | JWT 密钥 | `change-me-to-a-secure-key` |
| `DATABASE_URL` | PostgreSQL 连接串 | `postgresql+asyncpg://user:pass@host:5432/db` |
| `REDIS_URL` | Redis 连接串 | `redis://localhost:6379/0` |
| `OPENAI_API_KEY` | OpenAI API Key | `sk-...` |
| `SENSNOVA_API_KEY` | 星火大模型 API Key | `...` |
| `DEEPL_API_KEY` | DeepL API Key | `...` |
| `SENSENOVA_API_KEY` | 商汤/星火大模型 API Key | `...` |
| `NVIDIA_API_KEY` | NVIDIA 大模型 API Key | `...` |
| `OPENAI_API_KEY` | OpenAI 兼容 API Key(可选) | `sk-...` |
| `WECHAT_APP_ID` | 微信小程序 AppID | `wx...` |
| `WECHAT_APP_SECRET` | 微信小程序 AppSecret | `...` |
| `FRONTEND_URL` | 前端地址 | `http://localhost:5173` |
| `FRONTEND_URL` | 前端地址(逗号分隔多端) | `http://localhost:5173,http://localhost:5174` |
### 数据库初始化
@@ -217,7 +229,7 @@ alembic upgrade head
|------|------|
| 后端 | FastAPI + SQLAlchemy 1.4 async + asyncpg |
| 数据库 | PostgreSQL 15 + pgvector + Redis 7 |
| AI 提供商 | Sensenova (星火), OpenAI, DeepL |
| AI 提供商 | Sensenova (商汤/星火), NVIDIA, 阿里机器翻译 (alibaba-mt) |
| 前端 | Vue 3 + uni-app + Element Plus |
| 任务队列 | Celery + Redis |
| 容器化 | Docker + Docker Compose |
@@ -243,6 +255,9 @@ trade-assistant/
├── uni-app/ # 移动端 H5 + 小程序
├── admin-frontend/ # PC 管理后台
├── user-frontend/ # 用户工作台
├── browser-extension/ # Chrome 浏览器插件
├── landing/ # SEO 营销落地页(根路径 /+ robots.txt + sitemap.xml
├── deploy/ # 部署配置(含 frontend/nginx.conf 四路布局)
├── nginx/ # Nginx 配置
├── docs/ # 项目文档
│ ├── API_DESIGN.md
@@ -255,6 +270,37 @@ trade-assistant/
---
## 🌐 生产部署
站点 `trade.yuzhiran.com` 采用四路 Nginx 布局,由 `deploy/frontend/nginx.conf` 定义:
| 路径 | 内容 | 来源 |
|------|------|------|
| `/` | SEO 营销落地页(根) | `landing/index.html` + `robots.txt` + `sitemap.xml` |
| `/app/` | 移动端 H5uni-appbase `/app/` | `uni-app` 构建产物 |
| `/workspace/` | 用户工作台(user-frontendbase `/workspace/` | `user-frontend` 构建产物 |
| `/admin/` | 管理后台(admin-frontendbase `/admin/` | `admin-frontend` 构建产物 |
| `/api/` | 后端 API 反向代理 | `127.0.0.1:8000`systemd/uvicorn 运行) |
```bash
# 落地页(SEO 主入口)
cp landing/index.html landing/robots.txt landing/sitemap.xml /www/wwwroot/trade.yuzhiran.com/
# 三端 SPA
cd uni-app && npm run build:h5 && cp -r dist/build/h5/* /www/wwwroot/trade.yuzhiran.com/app/
cd user-frontend && npm run build && cp -r dist/* /www/wwwroot/trade.yuzhiran.com/workspace/
cd admin-frontend && npm run build && cp -r dist/* /www/wwwroot/trade.yuzhiran.com/admin/
# 后端迁移 + 重启
cd backend && source venv/bin/activate && alembic upgrade head
sudo systemctl restart ftrade-backend.service
# 重载 Nginx(套用 deploy/frontend/nginx.conf 的四路布局)
sudo nginx -t && sudo nginx -s reload
```
---
## 🧪 测试
```bash
+4 -1
View File
@@ -29,8 +29,11 @@ export function searchUsers(query) { return http.post('/admin/users/search', { q
export function listUsers(page = 1, size = 20) { return http.get('/admin/users', { params: { page, size } }) }
export function getUserDetail(id) { return http.get(`/admin/users/${id}`) }
export function updateUser(id, data) { return http.put(`/admin/users/${id}`, data) }
export function updateUserTier(id, tier) { return http.patch(`/admin/users/${id}/tier`, { tier }) }
export function toggleUserActive(id) { return http.post(`/admin/users/${id}/toggle-active`) }
export function updateUserRole(id, role) { return http.patch(`/admin/users/${id}/role`, { role }) }
export function getUsageStats() { return http.get('/admin/stats/usage') }
export function getUsageStats() { return http.get('/admin/usage-stats') }
export function listLogs(params) { return http.get('/admin/logs', { params }) }
+4 -4
View File
@@ -65,7 +65,7 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { listUsers, searchUsers, updateUser } from '@/api'
import { listUsers, searchUsers, updateUserTier, toggleUserActive, updateUserRole } from '@/api'
const loading = ref(false)
const searching = ref(false)
@@ -97,16 +97,16 @@ async function doSearch() {
}
async function changeTier(row, tier) {
try { await updateUser(row.id, { tier }); row.tier = tier; ElMessage.success('已更新') }
try { await updateUserTier(row.id, tier); row.tier = tier; ElMessage.success('已更新') }
catch (e) { ElMessage.error(e?.detail || '操作失败') }
}
async function toggleActive(row) {
try { await updateUser(row.id, { is_active: !row.is_active }); row.is_active = !row.is_active; ElMessage.success('已更新') }
try { await toggleUserActive(row.id); row.is_active = !row.is_active; ElMessage.success('已更新') }
catch (e) { ElMessage.error(e?.detail || '操作失败') }
}
async function toggleRole(row) {
const role = row.role === 'admin' ? 'user' : 'admin'
try { await updateUser(row.id, { role }); row.role = role; ElMessage.success('已更新') }
try { await updateUserRole(row.id, role); row.role = role; ElMessage.success('已更新') }
catch (e) { ElMessage.error(e?.detail || '操作失败') }
}
@@ -0,0 +1,40 @@
"""add enterprise_leads table
Revision ID: add_enterprise_leads
Revises: add_perf_indexes
Create Date: 2026-07-11
Stores inbound leads from the private-deployment / source-buyout CTAs.
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'add_enterprise_leads'
down_revision = 'add_perf_indexes'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
'enterprise_leads',
sa.Column('id', sa.String(36), primary_key=True),
sa.Column('type', sa.String(20), nullable=False, server_default='private'),
sa.Column('name', sa.String(100), nullable=False, server_default=''),
sa.Column('company', sa.String(200), nullable=False, server_default=''),
sa.Column('phone', sa.String(50), nullable=False, server_default=''),
sa.Column('email', sa.String(200), nullable=False, server_default=''),
sa.Column('message', sa.Text(), nullable=False, server_default=''),
sa.Column('status', sa.String(20), nullable=False, server_default='new'),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index('ix_enterprise_leads_type', 'enterprise_leads', ['type'])
op.create_index('ix_enterprise_leads_status', 'enterprise_leads', ['status'])
def downgrade() -> None:
op.drop_index('ix_enterprise_leads_status', table_name='enterprise_leads')
op.drop_index('ix_enterprise_leads_type', table_name='enterprise_leads')
op.drop_table('enterprise_leads')
+34
View File
@@ -0,0 +1,34 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel, EmailStr
from app.database import get_db
from app.models.enterprise_lead import EnterpriseLead
router = APIRouter()
class LeadCreate(BaseModel):
type: str # private | buyout
name: str
company: str = ""
phone: str = ""
email: str = ""
message: str = ""
@router.post("")
async def create_lead(payload: LeadCreate, db: AsyncSession = Depends(get_db)):
if payload.type not in ("private", "buyout"):
raise HTTPException(status_code=400, detail="Invalid lead type")
lead = EnterpriseLead(
type=payload.type,
name=payload.name,
company=payload.company,
phone=payload.phone,
email=payload.email,
message=payload.message,
)
db.add(lead)
await db.flush()
await db.refresh(lead)
return {"id": lead.id, "type": lead.type, "status": lead.status}
+1
View File
@@ -26,6 +26,7 @@ CSRF_SKIP_ENDPOINTS = [
"/api/v1/payment/",
"/api/v1/whatsapp/webhook",
"/api/v1/ai/",
"/api/v1/leads",
]
+2 -1
View File
@@ -129,7 +129,7 @@ async def health():
return {"status": "ok", "app": settings.APP_NAME, "version": "1.0.0"}
from app.api.v1 import auth, marketing, translate, customer, quotation, whatsapp, product, exchange, push, admin, analytics, teams, onboarding, notification, feedback, payment, interaction, silent_pattern, training, followup, ai_assistant, discovery, discovery_record, certification, invoice, usage, referral, admin_search, search, admin_ai, credits, admin_credits, agent
from app.api.v1 import auth, marketing, translate, customer, quotation, whatsapp, product, exchange, push, admin, analytics, teams, onboarding, notification, feedback, payment, interaction, silent_pattern, training, followup, ai_assistant, discovery, discovery_record, certification, invoice, usage, referral, admin_search, search, admin_ai, credits, admin_credits, agent, leads
app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"])
app.include_router(marketing.router, prefix="/api/v1/marketing", tags=["marketing"])
@@ -165,6 +165,7 @@ app.include_router(admin_credits.router, prefix="/api/v1/admin", tags=["admin"])
app.include_router(credits.router, prefix="/api/v1/credits", tags=["credits"])
app.include_router(search.router, prefix="/api/v1/search", tags=["search"])
app.include_router(agent.router, prefix="/api/v1/agent", tags=["agent"])
app.include_router(leads.router, prefix="/api/v1/leads", tags=["leads"])
if __name__ == "__main__":
+2
View File
@@ -24,6 +24,7 @@ from .user_credit import UserCredit
from .credit_consumption import CreditConsumption
from .credit_purchase import CreditPurchase
from .agent_pipeline import AgentPipeline
from .enterprise_lead import EnterpriseLead
__all__ = [
"User", "Product",
@@ -47,4 +48,5 @@ __all__ = [
"CreditConsumption",
"CreditPurchase",
"AgentPipeline",
"EnterpriseLead",
]
+17
View File
@@ -0,0 +1,17 @@
from sqlalchemy import Column, String, Text, DateTime, func
from app.database import Base
import uuid
class EnterpriseLead(Base):
__tablename__ = "enterprise_leads"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
type = Column(String(20), nullable=False, default="private") # private | buyout
name = Column(String(100), nullable=False, default="")
company = Column(String(200), nullable=False, default="")
phone = Column(String(50), nullable=False, default="")
email = Column(String(200), nullable=False, default="")
message = Column(Text, nullable=False, default="")
status = Column(String(20), nullable=False, default="new") # new | contacted | done
created_at = Column(DateTime(timezone=True), server_default=func.now())
+36
View File
@@ -0,0 +1,36 @@
import pytest
from httpx import AsyncClient
class TestLeadsAPI:
async def test_create_private_lead(self, client: AsyncClient):
res = await client.post(
"/api/v1/leads",
json={
"type": "private",
"name": "张三",
"company": "测试外贸公司",
"phone": "13800138000",
"message": "想私有化部署",
},
)
assert res.status_code == 200
data = res.json()
assert data["type"] == "private"
assert data["status"] == "new"
assert data["id"]
async def test_create_buyout_lead(self, client: AsyncClient):
res = await client.post(
"/api/v1/leads",
json={"type": "buyout", "name": "李四", "phone": "13900139000"},
)
assert res.status_code == 200
assert res.json()["type"] == "buyout"
async def test_invalid_type_rejected(self, client: AsyncClient):
res = await client.post(
"/api/v1/leads",
json={"type": "wrong", "name": "x", "phone": "1"},
)
assert res.status_code == 400
+7
View File
@@ -136,3 +136,10 @@ export async function generateKeywords(productName, description, count = 10) {
body: { product_name: productName, description, count, language: 'en' },
});
}
/** Extract structured customer info from pasted text */
export async function extractInfo(text, extractType = 'auto') {
return request('/api/v1/translate/extract', {
body: { text, extract_type: extractType },
});
}
+15
View File
@@ -42,6 +42,7 @@
<button class="tab" data-tab="reply">回复</button>
<button class="tab" data-tab="discovery">发现</button>
<button class="tab" data-tab="marketing">营销</button>
<button class="tab" data-tab="extract">客户</button>
</div>
<!-- Tab: Translate -->
@@ -99,6 +100,20 @@
</div>
<div id="marketing-result" class="result-box"></div>
</div>
<!-- Tab: Extract customer info -->
<div id="tab-extract" class="tab-content">
<textarea id="extract-input" rows="5" placeholder="粘贴客户名片 / 邮件签名 / 网页介绍,自动提取姓名、公司、邮箱、电话、国家等"></textarea>
<div class="row">
<select id="extract-type">
<option value="auto">自动识别</option>
<option value="contact">联系人</option>
<option value="company">公司</option>
</select>
<button id="extract-btn" class="btn btn-primary">提取客户</button>
</div>
<div id="extract-result" class="result-box"></div>
</div>
</div>
<!-- Footer -->
+30 -2
View File
@@ -7,6 +7,7 @@ import {
generateReply,
searchLeads,
generateMarketing,
extractInfo,
getBalance,
getSubscriptionPlans,
getCreditPackages,
@@ -53,6 +54,11 @@ const marketingStyle = $('marketing-style');
const marketingBtn = $('marketing-btn');
const marketingResult = $('marketing-result');
const extractInput = $('extract-input');
const extractType = $('extract-type');
const extractBtn = $('extract-btn');
const extractResult = $('extract-result');
const creditsDisplay = $('credits-display');
const btnSettings = $('btn-settings');
const upgradeBanner = $('upgrade-banner');
@@ -215,7 +221,7 @@ function setupUpgradeUI() {
// Open backend payment page or settings page
// For now, open the workspace credits page
chrome.tabs.create({
url: `${settingsUrl.value?.replace(/\/+$/, '') || 'https://trade.yuzhiran.com'}/workspace/credits`,
url: `${settingsUrl.value?.replace(/\/+$/, '') || 'https://trade.yuzhiran.com'}/workspace/upgrade`,
});
hideUpgradeModal();
});
@@ -225,7 +231,7 @@ function setupUpgradeUI() {
btn.addEventListener('click', () => {
const pkg = btn.dataset.pkg;
chrome.tabs.create({
url: `${settingsUrl.value?.replace(/\/+$/, '') || 'https://trade.yuzhiran.com'}/workspace/credits`,
url: `${settingsUrl.value?.replace(/\/+$/, '') || 'https://trade.yuzhiran.com'}/workspace/upgrade`,
});
hidePackageModal();
});
@@ -465,6 +471,28 @@ function setupActions() {
}
});
});
// Extract customer info
extractBtn.addEventListener('click', async () => {
const text = extractInput.value.trim();
if (!text) return;
setLoading(extractBtn, true);
showResult(extractResult, '<div class="loading"><span class="spinner"></span>提取中...</div>');
try {
const result = await extractInfo(text, extractType.value);
const info = result.extracted || {};
const rows = Object.entries(info)
.filter(([, v]) => v !== null && v !== undefined && v !== '')
.map(([k, v]) => `<div class="item"><div class="item-label">${escapeHtml(k)}</div><div>${escapeHtml(String(Array.isArray(v) ? v.join(', ') : v))}</div></div>`)
.join('');
showResult(extractResult, rows || '(无提取结果)');
refreshCredits();
} catch (err) {
handleApiError(extractResult, err);
} finally {
setLoading(extractBtn, false);
}
});
}
// Auto-init on load
+40 -25
View File
@@ -1,7 +1,15 @@
# 宝塔面板 Nginx 配置
# 宝塔面板 Nginx 配置 / 通用 Nginx 配置
# 注意:请勿直接覆盖宝塔生成的配置文件!
# 将此配置中的 server 块复制到宝塔对应站点的配置中
# 宝塔路径: /www/server/panel/vhost/nginx/trade.yuzhiran.com.conf
# 将此 server 块复制到宝塔对应站点(宝塔路径: /www/server/panel/vhost/nginx/trade.yuzhiran.com.conf
#
# 部署目录结构(/www/wwwroot/trade.yuzhiran.com/):
# index.html <- 营销落地页(landing/ 产物)
# robots.txt <- landing/robots.txt
# sitemap.xml <- landing/sitemap.xml
# app/ <- uni-app H5 构建产物(base: /app/
# workspace/ <- user-frontend 构建产物(base: /workspace/
# admin/ <- admin-frontend 构建产物(base: /admin/
# backend/ <- FastAPI 后端(由 supervisor/uvicorn 运行,经 /api/ 代理)
server {
listen 80;
@@ -16,12 +24,11 @@ server {
# SSL 证书(宝塔面板中配置,或取消注释以下行)
# ssl_certificate /www/server/panel/vhost/cert/trade.yuzhiran.com/fullchain.pem;
# ssl_certificate_key /www/server/panel/vhost/cert/trade.yuzhiran.com/privkey.pem;
# ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
# ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:!MD5;
# ssl_prefer_server_ciphers on;
# 前端静态文件(uni-app build:h5 产物)
root /www/wwwroot/trade.yuzhiran.com/frontend/dist;
root /www/wwwroot/trade.yuzhiran.com;
index index.html;
# gzip
@@ -30,6 +37,26 @@ server {
gzip_comp_level 6;
gzip_types text/plain text/css text/javascript application/json application/javascript image/svg+xml;
# 营销落地页(根路径,SEO 收录主入口)
location / {
try_files $uri $uri/ /index.html;
}
# 移动端 H5uni-app, base: /app/
location /app/ {
try_files $uri $uri/ /app/index.html;
}
# 网页工作台(user-frontend, base: /workspace/
location /workspace/ {
try_files $uri $uri/ /workspace/index.html;
}
# 管理后台(admin-frontend, base: /admin/
location /admin/ {
try_files $uri $uri/ /admin/index.html;
}
# API 反向代理到后端
location /api/ {
proxy_pass http://127.0.0.1:8000;
@@ -39,32 +66,20 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
# WebSocket 支持(如有需要)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# 上传文件(如有需要可配置单独的路径)
# location /uploads/ {
# alias /www/wwwroot/trade.yuzhiran.com/backend/uploads/;
# expires 7d;
# }
# SPA 路由 fallback
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
expires 7d;
add_header Cache-Control "public, immutable";
}
# 禁止访问隐藏文件
location ~ /\. {
deny all;
# 证书续期校验
location ~ \.well-known {
root /www/wwwroot/trade.yuzhiran.com;
allow all;
}
}
+324
View File
@@ -0,0 +1,324 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>TradeMate 外贸小助手 — AI 驱动的外贸全流程工作台 | 免费试用·私有化部署·买断源码</title>
<meta name="description" content="TradeMate 外贸小助手,专为外贸 SOHO 与小微团队打造的 AI 工作台:智能中英翻译、客户挖掘、营销文案、报价单自动生成、AI 数字员工自动开发客户。支持免费试用、私有化部署与买断源码,数据私有、合规可控。" />
<meta name="keywords" content="外贸小助手,外贸AI工具,AI翻译,客户开发,外贸营销,报价单生成,客户管理,AI数字员工,私有化部署,买断源码,外贸SOHO,外贸获客" />
<meta name="author" content="北京宇之然科技中心" />
<meta name="robots" content="index, follow, max-image-preview:large" />
<meta name="theme-color" content="#1890ff" />
<link rel="canonical" href="https://trade.yuzhiran.com/" />
<link rel="icon" href="/favicon.ico" />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:locale" content="zh_CN" />
<meta property="og:title" content="TradeMate 外贸小助手 — AI 驱动的外贸全流程工作台" />
<meta property="og:description" content="智能翻译·客户挖掘·营销生成·报价单·AI数字员工。免费试用 / 私有化部署 / 买断源码,数据私有合规。" />
<meta property="og:url" content="https://trade.yuzhiran.com/" />
<meta property="og:site_name" content="TradeMate 外贸小助手" />
<meta property="og:image" content="https://trade.yuzhiran.com/app/static/images/yzr/yuzhiran.jpg" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="TradeMate 外贸小助手 — AI 驱动的外贸全流程工作台" />
<meta name="twitter:description" content="智能翻译·客户挖掘·营销生成·报价单·AI数字员工。免费试用 / 私有化部署 / 买断源码。" />
<meta name="twitter:image" content="https://trade.yuzhiran.com/app/static/images/yzr/yuzhiran.jpg" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "TradeMate 外贸小助手",
"operatingSystem": "Web, Android, iOS, Browser Extension",
"applicationCategory": "BusinessApplication",
"url": "https://trade.yuzhiran.com/",
"description": "AI 驱动的外贸全流程工作台:智能翻译、客户挖掘、营销文案、报价单生成、客户健康度与 AI 数字员工自动开发客户。",
"offers": [
{ "@type": "Offer", "name": "免费试用", "price": "0", "priceCurrency": "CNY" },
{ "@type": "Offer", "name": "私有化部署", "price": "39800", "priceCurrency": "CNY", "description": "年付授权,独立部署到客户自有服务器" },
{ "@type": "Offer", "name": "买断源码", "price": "98000", "priceCurrency": "CNY", "description": "一次性买断完整前后端源码,可二次开发" }
],
"publisher": { "@type": "Organization", "name": "北京宇之然科技中心", "url": "https://trade.yuzhiran.com/" }
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{ "@type": "Question", "name": "TradeMate 适合哪些人使用?", "acceptedAnswer": { "@type": "Answer", "text": "主要面向外贸 SOHO、小微外贸团队与业务员,帮助完成翻译、找客户、写营销、做报价等日常工作。" } },
{ "@type": "Question", "name": "免费试用包含什么?", "acceptedAnswer": { "@type": "Answer", "text": "免费版提供 30 积分与每日 1000 字免费翻译,可完整体验核心功能,零门槛上手。" } },
{ "@type": "Question", "name": "私有化部署和买断源码有什么区别?", "acceptedAnswer": { "@type": "Answer", "text": "私有化部署是按年付费的授权,系统独立部署在你的服务器、数据私有;买断源码是一次付费获得完整前后端源码,可自由二次开发、永久授权。" } },
{ "@type": "Question", "name": "我的客户数据安全吗?", "acceptedAnswer": { "@type": "Answer", "text": "支持私有化部署,数据完全保存在你自己的服务器;公有云版本也通过 JWT 鉴权与最小化权限保护数据。" } },
{ "@type": "Question", "name": "支持哪些平台?", "acceptedAnswer": { "@type": "Answer", "text": "提供网页工作台、移动端 H5、微信小程序与 Chrome 浏览器插件,数据账号互通。" } }
]
}
</script>
<style>
:root { --blue:#1890ff; --blue-d:#096dd9; --ink:#1e293b; --sub:#64748b; --bg:#f6f8fb; --line:#e5e7eb; }
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin:0; font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif; color:var(--ink); background:var(--bg); line-height:1.7; }
a { color:inherit; text-decoration:none; }
.wrap { max-width:1080px; margin:0 auto; padding:0 20px; }
header.nav { position:sticky; top:0; z-index:50; background:rgba(255,255,255,.92); backdrop-filter:blur(8px); border-bottom:1px solid var(--line); }
.nav-in { display:flex; align-items:center; justify-content:space-between; height:62px; }
.logo { font-weight:800; font-size:20px; color:var(--blue); display:flex; align-items:center; gap:8px; }
.logo svg { width:28px; height:28px; }
.nav-links { display:flex; gap:22px; font-size:15px; color:var(--sub); }
.nav-links a:hover { color:var(--blue); }
.nav-cta { display:flex; gap:10px; }
.btn { display:inline-block; padding:10px 20px; border-radius:10px; font-weight:600; font-size:15px; cursor:pointer; border:1px solid transparent; }
.btn-primary { background:linear-gradient(135deg,var(--blue),var(--blue-d)); color:#fff; }
.btn-ghost { border-color:var(--blue); color:var(--blue); background:#fff; }
.btn:hover { opacity:.92; }
.hero { padding:74px 0 56px; text-align:center; background:radial-gradient(1200px 400px at 50% -10%, #e6f1ff, transparent); }
.hero h1 { font-size:42px; line-height:1.25; margin:0 0 16px; font-weight:800; }
.hero h1 .hl { color:var(--blue); }
.hero p { font-size:18px; color:var(--sub); max-width:720px; margin:0 auto 28px; }
.hero .cta-row { display:flex; gap:14px; justify-content:center; flex-wrap:wrap; }
.hero .badges { margin-top:26px; display:flex; gap:18px; justify-content:center; flex-wrap:wrap; color:var(--sub); font-size:14px; }
.hero .badges span { background:#fff; border:1px solid var(--line); padding:6px 14px; border-radius:999px; }
section { padding:60px 0; }
.sec-head { text-align:center; margin-bottom:38px; }
.sec-head h2 { font-size:30px; margin:0 0 10px; }
.sec-head p { color:var(--sub); margin:0; }
.pains { display:grid; grid-template-columns:repeat(3,1fr); gap:18px; }
.card { background:#fff; border:1px solid var(--line); border-radius:16px; padding:24px; }
.card h3 { margin:0 0 8px; font-size:18px; }
.card p { margin:0; color:var(--sub); font-size:14px; }
.pain .card { border-top:3px solid #ff7875; }
.feat { display:grid; grid-template-columns:repeat(3,1fr); gap:18px; }
.feat .card { border-top:3px solid var(--blue); }
.feat .ico { font-size:26px; }
.steps { display:grid; grid-template-columns:repeat(4,1fr); gap:16px; counter-reset:step; }
.steps .card { position:relative; }
.steps .card::before { counter-increment:step; content:counter(step); position:absolute; top:-14px; left:20px; width:30px; height:30px; background:var(--blue); color:#fff; border-radius:50%; display:flex; align-items:center; justify-content:center; font-weight:700; }
.price { display:grid; grid-template-columns:repeat(3,1fr); gap:18px; align-items:stretch; }
.price .card { display:flex; flex-direction:column; text-align:center; }
.price .pname { font-size:20px; font-weight:700; }
.price .pval { font-size:34px; font-weight:800; color:var(--blue); margin:10px 0; }
.price .pval small { font-size:15px; color:var(--sub); font-weight:500; }
.price ul { list-style:none; padding:0; margin:8px 0 18px; color:var(--sub); font-size:14px; }
.price ul li { padding:5px 0; border-bottom:1px dashed var(--line); }
.price .card.featured { border:2px solid var(--blue); box-shadow:0 8px 24px rgba(24,144,255,.12); }
.price .btn { margin-top:auto; }
.compare table { width:100%; border-collapse:collapse; background:#fff; border:1px solid var(--line); border-radius:14px; overflow:hidden; }
.compare th, .compare td { padding:14px 16px; text-align:left; border-bottom:1px solid var(--line); font-size:14px; }
.compare th { background:#f1f6ff; }
.faq details { background:#fff; border:1px solid var(--line); border-radius:12px; padding:14px 18px; margin-bottom:12px; }
.faq summary { font-weight:600; cursor:pointer; }
.faq p { color:var(--sub); margin:10px 0 0; }
.cta-final { text-align:center; background:linear-gradient(135deg,var(--blue),var(--blue-d)); color:#fff; border-radius:20px; padding:48px 20px; }
.cta-final h2 { margin:0 0 10px; }
.cta-final p { opacity:.92; margin:0 0 22px; }
.cta-final .btn-ghost { background:rgba(255,255,255,.15); color:#fff; border-color:#fff; }
footer { background:#0f172a; color:#cbd5e1; padding:40px 0; font-size:14px; }
footer a { color:#93c5fd; }
footer .cols { display:flex; gap:40px; flex-wrap:wrap; }
footer .cols div { min-width:160px; }
footer h4 { color:#fff; margin:0 0 10px; font-size:15px; }
.copyright { border-top:1px solid #1e293b; margin-top:24px; padding-top:16px; color:#94a3b8; font-size:13px; }
@media (max-width:860px){ .pains,.feat,.price,.steps{ grid-template-columns:1fr; } .nav-links{ display:none; } .hero h1{ font-size:30px; } }
</style>
</head>
<body>
<header class="nav">
<div class="wrap nav-in">
<a class="logo" href="/">
<svg viewBox="0 0 24 24" fill="none"><rect x="2" y="2" width="20" height="20" rx="5" fill="#1890ff"/><path d="M7 15l3-4 3 3 4-6" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
TradeMate
</a>
<nav class="nav-links">
<a href="#features">功能</a>
<a href="#how">怎么用</a>
<a href="#pricing">价格</a>
<a href="#faq">常见问题</a>
<a href="/app/">开始使用</a>
</nav>
<div class="nav-cta">
<a class="btn btn-ghost" href="/workspace/">登录</a>
<a class="btn btn-primary" href="/app/">免费试用</a>
</div>
</div>
</header>
<main>
<section class="hero">
<div class="wrap">
<h1>让 AI 替你跑完<span class="hl">外贸全流程</span><br/>翻译 · 找客户 · 写营销 · 做报价</h1>
<p>TradeMate 外贸小助手,把重复活交给 AI。一个人也能像一支外贸团队一样高效获客、转化与跟进。</p>
<div class="cta-row">
<a class="btn btn-primary" href="/app/">免费试用</a>
<a class="btn btn-ghost" href="#pricing">私有化部署 / 买断源码</a>
</div>
<div class="badges">
<span>🌐 中英等多语翻译</span>
<span>🤖 AI 数字员工自动开发客户</span>
<span>🔒 支持私有化部署·数据私有</span>
<span>🧩 网页 / H5 / 小程序 / 插件</span>
</div>
</div>
</section>
<section id="pains">
<div class="wrap">
<div class="sec-head"><h2>做外贸,这些事最耗你时间</h2><p>不是你不够努力,是工具没帮你把重复活自动化</p></div>
<div class="pains">
<div class="card pain"><h3>翻译来回复制粘贴</h3><p>询盘、邮件、资料中英切换,每天大量时间耗在基础翻译上。</p></div>
<div class="card pain"><h3>找不到靠谱客户</h3><p>Google 翻页、手动筛选,获客效率低、线索质量不稳。</p></div>
<div class="card pain"><h3>营销文案写不动</h3><p>开发信、社媒文案反复憋,风格不统一、回复率低。</p></div>
<div class="card pain"><h3>报价单手工拼</h3><p>从询盘到报价单复制粘贴,易错、慢、难追踪。</p></div>
<div class="card pain"><h3>客户跟进靠记忆</h3><p>沉默客户没提醒,跟进断层,到手的单也容易飞。</p></div>
<div class="card pain"><h3>团队协作用表格</h3><p>客户表满天飞,数据分散、权限混乱、难沉淀。</p></div>
</div>
</div>
</section>
<section id="features" style="background:#fff;border-top:1px solid var(--line);border-bottom:1px solid var(--line);">
<div class="wrap">
<div class="sec-head"><h2>一个工作台,覆盖外贸全流程</h2><p>从沟通到成交,AI 在每一步帮你提速</p></div>
<div class="feat">
<div class="card"><div class="ico">🌐</div><h3>智能翻译 / 回复</h3><p>中英等多语互译,按询盘一键生成多风格专业回复。</p></div>
<div class="card"><div class="ico">🔍</div><h3>AI 客户挖掘</h3><p>基于产品与市场的智能搜索,自动提取邮箱/电话/WhatsApp。</p></div>
<div class="card"><div class="ico">✍️</div><h3>营销文案生成</h3><p>开发信、社媒、关键词、竞品分析,多风格一键产出。</p></div>
<div class="card"><div class="ico">🧾</div><h3>报价单自动生成</h3><p>从询盘自动生成报价单,多币种、状态可追踪。</p></div>
<div class="card"><div class="ico">💡</div><h3>客户健康度</h3><p>沉默客户提醒、互动分析,帮你及时跟进不漏单。</p></div>
<div class="card"><div class="ico">🤖</div><h3>AI 数字员工</h3><p>输入产品+市场,自动 搜索→分析→触达→沉淀高匹配客户。</p></div>
</div>
</div>
</section>
<section id="how">
<div class="wrap">
<div class="sec-head"><h2>四步上手,当天见效</h2><p>无需培训,打开就能用</p></div>
<div class="steps">
<div class="card"><h3>注册 / 游客体验</h3><p>手机号或游客模式,免费体验核心功能。</p></div>
<div class="card"><h3>导入客户与产品</h3><p>录入产品或客户,AI 即刻理解你的业务上下文。</p></div>
<div class="card"><h3>用 AI 做日常</h3><p>翻译、写营销、生成报价、让数字员工去开发客户。</p></div>
<div class="card"><h3>跟进与转化</h3><p>健康度提醒 + 智能跟进,把线索变成订单。</p></div>
</div>
</div>
</section>
<section id="pricing" style="background:#fff;border-top:1px solid var(--line);border-bottom:1px solid var(--line);">
<div class="wrap">
<div class="sec-head"><h2>三种方式,按需选择</h2><p>公有云免费起步,企业可私有化或买断,数据自主可控</p></div>
<div class="price">
<div class="card">
<div class="pname">免费试用</div>
<div class="pval">¥0</div>
<ul>
<li>30 积分(一次性)</li>
<li>每日 1000 字免费翻译</li>
<li>翻译 / 回复 / 客户发现 / 营销</li>
<li>网页端 · 插件 · 小程序通用</li>
</ul>
<a class="btn btn-ghost" href="/app/">免费使用</a>
</div>
<div class="card featured">
<div class="pname">私有化部署</div>
<div class="pval">¥39,800<small>/年</small></div>
<ul>
<li>独立部署到你的服务器</li>
<li>数据完全私有、安全合规</li>
<li>不限账号与调用量</li>
<li>对接自有 AI 模型 · 一年支持</li>
</ul>
<a class="btn btn-primary" href="#contact">申请部署</a>
</div>
<div class="card">
<div class="pname">买断源码</div>
<div class="pval">¥98,000<small>/一次性</small></div>
<ul>
<li>完整前后端源码</li>
<li>可自由二次开发</li>
<li>永久授权 · 无后续费用</li>
<li>文档与社区支持</li>
</ul>
<a class="btn btn-ghost" href="#contact">咨询买断</a>
</div>
</div>
</div>
</section>
<section id="compare">
<div class="wrap">
<div class="sec-head"><h2>为什么选 TradeMate</h2></div>
<div class="compare">
<table>
<thead><tr><th>能力</th><th>TradeMate</th><th>纯人工 / 通用工具</th></tr></thead>
<tbody>
<tr><td>翻译 + 回复</td><td>AI 贴合外贸语境,一键多风格</td><td>复制粘贴、风格不一</td></tr>
<tr><td>客户开发</td><td>AI 数字员工自动搜索→触达→沉淀</td><td>手动翻页、效率低</td></tr>
<tr><td>营销文案</td><td>开发信/社媒/关键词批量生成</td><td>逐条手写、耗时</td></tr>
<tr><td>数据归属</td><td>支持私有化部署,数据私有</td><td>依赖第三方、不可控</td></tr>
<tr><td>多端协同</td><td>网页/H5/小程序/插件互通</td><td>工具割裂、难协同</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<section id="faq">
<div class="wrap">
<div class="sec-head"><h2>常见问题</h2></div>
<div class="faq">
<details open><summary>TradeMate 适合哪些人使用?</summary><p>主要面向外贸 SOHO、小微外贸团队与业务员,帮助完成翻译、找客户、写营销、做报价等日常工作。</p></details>
<details><summary>免费试用包含什么?</summary><p>免费版提供 30 积分与每日 1000 字免费翻译,可完整体验核心功能,零门槛上手。</p></details>
<details><summary>私有化部署和买断源码有什么区别?</summary><p>私有化部署是按年付费的授权,系统独立部署在你的服务器、数据私有;买断源码是一次付费获得完整前后端源码,可自由二次开发、永久授权。</p></details>
<details><summary>我的客户数据安全吗?</summary><p>支持私有化部署,数据完全保存在你自己的服务器;公有云版本也通过 JWT 鉴权与最小化权限保护数据。</p></details>
<details><summary>支持哪些平台?</summary><p>提供网页工作台、移动端 H5、微信小程序与 Chrome 浏览器插件,数据账号互通。</p></details>
</div>
</div>
</section>
<section id="contact">
<div class="wrap">
<div class="cta-final">
<h2>现在就开始,让 AI 帮你跑外贸</h2>
<p>免费试用零门槛,企业可私有化部署或买断源码,数据自主、合规可控。</p>
<div class="cta-row" style="display:flex;gap:14px;justify-content:center;flex-wrap:wrap;">
<a class="btn btn-primary" href="/app/">免费试用</a>
<a class="btn btn-ghost" href="/workspace/">网页工作台</a>
</div>
</div>
</div>
</section>
</main>
<footer>
<div class="wrap">
<div class="cols">
<div>
<h4>TradeMate</h4>
<p>AI 驱动的外贸全流程工作台</p>
</div>
<div>
<h4>产品</h4>
<p><a href="/app/">移动端 H5</a><br/><a href="/workspace/">网页工作台</a><br/><a href="#features">功能介绍</a></p>
</div>
<div>
<h4>价格</h4>
<p><a href="#pricing">免费试用</a><br/><a href="#pricing">私有化部署</a><br/><a href="#pricing">买断源码</a></p>
</div>
<div>
<h4>资源</h4>
<p><a href="#faq">常见问题</a><br/><a href="/app/pages/agreement/privacy">隐私政策</a><br/><a href="/app/pages/agreement/terms">用户协议</a></p>
</div>
</div>
<div class="copyright">
© 2026 北京宇之然科技中心 · TradeMate 外贸小助手 ·
<a href="/sitemap.xml">网站地图</a>
</div>
</div>
</footer>
</body>
</html>
+22
View File
@@ -0,0 +1,22 @@
User-agent: *
Allow: /
Allow: /app/
Allow: /workspace/
Disallow: /admin/
Disallow: /api/
User-agent: Googlebot
Allow: /
Allow: /app/
Allow: /workspace/
Disallow: /admin/
Disallow: /api/
User-agent: Baiduspider
Allow: /
Allow: /app/
Allow: /workspace/
Disallow: /admin/
Disallow: /api/
Sitemap: https://trade.yuzhiran.com/sitemap.xml
+34
View File
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemap.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://trade.yuzhiran.com/</loc>
<lastmod>2026-07-11</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://trade.yuzhiran.com/app/</loc>
<lastmod>2026-07-11</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://trade.yuzhiran.com/workspace/</loc>
<lastmod>2026-07-11</lastmod>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://trade.yuzhiran.com/app/pages/agreement/privacy</loc>
<lastmod>2026-07-11</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://trade.yuzhiran.com/app/pages/agreement/terms</loc>
<lastmod>2026-07-11</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
</urlset>
-2
View File
@@ -1,2 +0,0 @@
node_modules/
dist/
-20
View File
@@ -1,20 +0,0 @@
{
"name": "opencode-search-mcp",
"version": "1.0.0",
"description": "MCP server wrapping opencode search capabilities",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"@opencode-ai/sdk": "^1.14.41",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0"
}
}
-139
View File
@@ -1,139 +0,0 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createOpencodeClient } from "@opencode-ai/sdk";
import { z } from "zod";
const server = new McpServer({
name: "opencode-search",
version: "1.0.0",
});
const client = createOpencodeClient({
baseUrl: process.env.OPENCODE_URL || "http://127.0.0.1:4096",
});
server.registerTool(
"search_files",
{
title: "Search Files",
description: "Search for files and directories by name in the opencode workspace",
inputSchema: z.object({
query: z.string(),
directory: z.string().optional(),
limit: z.number().min(1).max(200).optional(),
}),
},
async ({ query, directory, limit }) => {
try {
const results = await (client.find.files as any)({
query: {
query,
directory: directory || undefined,
limit: limit || 50,
},
});
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
} catch (e) {
return {
content: [{ type: "text", text: `Error: ${(e as Error).message}` }],
isError: true,
};
}
}
);
server.registerTool(
"search_text",
{
title: "Search Text",
description: "Search for text content within files using regex patterns",
inputSchema: z.object({
pattern: z.string(),
directory: z.string().optional(),
}),
},
async ({ pattern, directory }) => {
try {
const results = await (client.find.text as any)({
query: {
pattern,
directory: directory || undefined,
},
});
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
} catch (e) {
return {
content: [{ type: "text", text: `Error: ${(e as Error).message}` }],
isError: true,
};
}
}
);
server.registerTool(
"search_symbols",
{
title: "Search Symbols",
description: "Search for code symbols in the workspace",
inputSchema: z.object({
pattern: z.string(),
directory: z.string().optional(),
limit: z.number().min(1).max(200).optional(),
}),
},
async ({ pattern, directory, limit }) => {
try {
const results = await (client.find.symbols as any)({
query: {
pattern,
directory: directory || undefined,
limit: limit || 50,
},
});
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
} catch (e) {
return {
content: [{ type: "text", text: `Error: ${(e as Error).message}` }],
isError: true,
};
}
}
);
server.registerTool(
"get_workspace_path",
{
title: "Get Workspace Path",
description: "Get the current opencode workspace path info",
inputSchema: z.object({}),
},
async () => {
try {
const path = await client.path.get();
return {
content: [{ type: "text", text: JSON.stringify(path, null, 2) }],
};
} catch (e) {
return {
content: [{ type: "text", text: `Error: ${(e as Error).message}` }],
isError: true,
};
}
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((e) => {
console.error("MCP server error:", e);
process.exit(1);
});
-13
View File
@@ -1,13 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
+8 -8
View File
@@ -6,7 +6,7 @@
"": {
"name": "trademate-tests",
"dependencies": {
"playwright": "^1.60.0"
"playwright": "^1.61.1"
}
},
"node_modules/fsevents": {
@@ -24,12 +24,12 @@
}
},
"node_modules/playwright": {
"version": "1.60.0",
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.60.0.tgz",
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
"version": "1.61.1",
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.61.1.tgz",
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.60.0"
"playwright-core": "1.61.1"
},
"bin": {
"playwright": "cli.js"
@@ -42,9 +42,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.60.0",
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.60.0.tgz",
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
"version": "1.61.1",
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.61.1.tgz",
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
+1 -1
View File
@@ -5,6 +5,6 @@
"test": "node test_all.mjs"
},
"dependencies": {
"playwright": "^1.60.0"
"playwright": "^1.61.1"
}
}
+10 -10
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, viewport-fit=cover" />
<meta name="description" content="TradeMate 外贸小助手 - AI 驱动的外贸智能工作台。智能翻译、客户管理、营销文案、报价单生成、WhatsApp 集成,专为外贸 SOHO 和小团队打造。" />
<meta name="description" content="TradeMate 外贸小助手 - AI 驱动的外贸智能工作台。智能翻译、客户管理、营销文案、报价单生成、AI 数字员工,支持免费试用、私有化部署与买断源码,专为外贸 SOHO 和小团队打造。" />
<meta name="keywords" content="外贸小助手,TradeMate,外贸AI工具,智能翻译,客户管理,营销文案,报价单,WhatsApp集成,外贸SOHO,外贸工具" />
<meta name="author" content="北京宇之然科技中心" />
<meta name="robots" content="index, follow" />
@@ -18,11 +18,11 @@
<meta property="og:description" content="专为外贸 SOHO 和小团队打造的 AI 智能工作台。集成智能翻译、客户管理、营销文案、报价单、WhatsApp 沟通于一体。" />
<meta property="og:url" content="https://trade.yuzhiran.com/app/" />
<meta property="og:site_name" content="TradeMate" />
<meta property="og:image" content="https://trade.yuzhiran.com/og-image.png" />
<meta property="og:image" content="https://trade.yuzhiran.com/app/static/images/yzr/yuzhiran.jpg" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="TradeMate 外贸小助手" />
<meta name="twitter:description" content="AI 驱动的外贸智能工作台" />
<meta name="twitter:image" content="https://trade.yuzhiran.com/og-image.png" />
<meta name="twitter:description" content="AI 驱动的外贸智能工作台,支持免费试用、私有化部署与买断源码" />
<meta name="twitter:image" content="https://trade.yuzhiran.com/app/static/images/yzr/yuzhiran.jpg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
@@ -39,12 +39,12 @@
"url": "https://trade.yuzhiran.com/app/",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web, iOS, Android",
"description": "AI 驱动的外贸智能工作台,专为外贸 SOHO 和小团队打造",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "CNY"
},
"description": "AI 驱动的外贸智能工作台,专为外贸 SOHO 和小团队打造,支持免费试用、私有化部署与买断源码",
"offers": [
{ "@type": "Offer", "name": "免费试用", "price": "0", "priceCurrency": "CNY" },
{ "@type": "Offer", "name": "私有化部署", "price": "39800", "priceCurrency": "CNY", "description": "年付授权,独立部署到客户自有服务器" },
{ "@type": "Offer", "name": "买断源码", "price": "98000", "priceCurrency": "CNY", "description": "一次性买断完整前后端源码,可二次开发" }
],
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.8",
+63 -68
View File
@@ -1,7 +1,7 @@
<template>
<view class="container">
<view class="header">
<text class="page-title">升级会员</text>
<text class="page-title">选择方案</text>
<text class="current-plan" v-if="currentPlan">当前: {{ planLabel(currentPlan) }}</text>
</view>
@@ -16,8 +16,7 @@
<text class="plan-name">{{ plan.name }}</text>
<text class="plan-price">
<text class="price-num">¥{{ plan.price }}</text>
<text class="price-unit" v-if="plan.price > 0">/</text>
<text class="price-unit" v-else>免费</text>
<text class="price-unit">{{ plan.unit }}</text>
</text>
<view class="plan-features">
<text class="feature" v-for="(f, i) in plan.features" :key="i"> {{ f }}</text>
@@ -26,92 +25,87 @@
</view>
</view>
<button
class="upgrade-btn"
@click="handleUpgrade"
:disabled="!selected || selected === currentPlan || loading"
>
{{ loading ? '处理中...' : (selected === currentPlan ? '当前方案' : '立即升级') }}
<button class="upgrade-btn" @click="handleSelect" :disabled="!selected || loading">
{{ loading ? '处理中...' : actionText }}
</button>
<!-- H5 Native 支付显示二维码 -->
<view class="qr-modal" v-if="showQr">
<!-- Enterprise contact popup -->
<view class="qr-modal" v-if="showContact">
<view class="qr-box">
<text class="qr-title">请使用微信扫码支付</text>
<image class="qr-img" :src="qrCodeUrl" mode="widthFix" />
<text class="qr-hint">打开微信扫一扫完成支付</text>
<text class="qr-close" @click="showQr = false">关闭</text>
<text class="qr-title">{{ contactTitle }}</text>
<input class="c-input" v-model="form.name" placeholder="您的称呼" />
<input class="c-input" v-model="form.company" placeholder="公司名称(选填)" />
<input class="c-input" v-model="form.phone" placeholder="手机号(必填)" />
<textarea class="c-area" v-model="form.message" placeholder="部署规模 / 定制需求(选填)" />
<button class="c-submit" :disabled="contactLoading" @click="submitContact">提交申请</button>
<text class="qr-close" @click="showContact = false">关闭</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { paymentApi } from '@/utils/api.js'
import { leadApi } from '@/utils/api.js'
const plans = ref([])
const PRICING = [
{ id: 'free', name: '免费试用', price: '0', unit: '', features: ['30 积分(一次性)', '每日 1000 字免费翻译', '网页端 / 插件通用'], action: 'trial' },
{ id: 'private', name: '私有化部署', price: '39,800', unit: '/年', features: ['独立部署、数据私有', '不限账号与调用量', '对接自有 AI 模型', '一年技术支持'], action: 'contact' },
{ id: 'buyout', name: '买断源码', price: '98,000', unit: '/一次性', features: ['完整前后端源码', '可二次开发', '永久授权', '文档与社区支持'], action: 'contact' },
]
const plans = ref(PRICING)
const currentPlan = ref('free')
const selected = ref('')
const loading = ref(false)
const showQr = ref(false)
const qrCodeUrl = ref('')
onShow(async () => {
try {
const [planRes, subRes] = await Promise.all([
paymentApi.plans(),
paymentApi.subscription(),
])
plans.value = planRes.plans || []
currentPlan.value = subRes.plan || 'free'
} catch (_) {}
const showContact = ref(false)
const contactLoading = ref(false)
const contactType = ref('private')
const form = ref({ name: '', company: '', phone: '', message: '' })
const contactTitle = computed(() => contactType.value === 'private' ? '申请私有化部署' : '咨询源码买断')
const actionText = computed(() => {
const p = plans.value.find(x => x.id === selected.value)
if (!p) return '请选择方案'
if (p.action === 'trial') return '免费使用'
return contactType.value === 'private' ? '申请私有化部署' : '咨询源码买断'
})
onShow(() => { currentPlan.value = 'free' })
const planLabel = (id) => {
const map = { free: '免费版', pro: 'Pro 版', enterprise: '企业版' }
const map = { free: '免费版', private: '私有化部署', buyout: '买断源码' }
return map[id] || id
}
const handleUpgrade = async () => {
if (!selected.value || selected.value === currentPlan.value) return
loading.value = true
try {
// #ifdef MP-WEIXIN
const payType = 'jsapi'
// #endif
// #ifdef H5
const payType = 'native'
// #endif
const res = await paymentApi.createOrder(selected.value, payType)
if (res.amount === 0) {
uni.showToast({ title: '已切换为免费版', icon: 'success' })
currentPlan.value = selected.value
const handleSelect = () => {
const p = plans.value.find(x => x.id === selected.value)
if (!p) return
if (p.action === 'trial') {
uni.showToast({ title: '已使用免费版', icon: 'success' })
return
}
if (res.pay_type === 'jsapi' && res.pay_params) {
uni.requestPayment({
provider: 'wxpay',
...res.pay_params,
success: () => {
uni.showToast({ title: '支付成功', icon: 'success' })
currentPlan.value = selected.value
},
fail: (err) => {
uni.showToast({ title: err.errMsg || '支付失败', icon: 'none' })
},
})
} else if (res.pay_type === 'native' && res.code_url) {
qrCodeUrl.value = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(res.code_url)}`
showQr.value = true
contactType.value = p.id
form.value = { name: '', company: '', phone: '', message: '' }
showContact.value = true
}
const submitContact = async () => {
if (!form.value.name.trim() || !form.value.phone.trim()) {
uni.showToast({ title: '请填写称呼与手机号', icon: 'none' })
return
}
contactLoading.value = true
try {
await leadApi.create({ type: contactType.value, ...form.value })
uni.showToast({ title: '提交成功,商务会联系您', icon: 'success' })
showContact.value = false
} catch (err) {
uni.showToast({ title: err.message || '操作失败', icon: 'none' })
uni.showToast({ title: err.message || '提交失败', icon: 'none' })
} finally {
loading.value = false
contactLoading.value = false
}
}
</script>
@@ -136,9 +130,10 @@ const handleUpgrade = async () => {
.upgrade-btn[disabled] { background: #a0cfff; }
.qr-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 999; display: flex; align-items: center; justify-content: center; }
.qr-box { background: #fff; border-radius: 20rpx; padding: 50rpx; text-align: center; width: 500rpx; }
.qr-title { font-size: 30rpx; font-weight: 600; color: #333; margin-bottom: 30rpx; display: block; }
.qr-img { width: 300rpx; height: 300rpx; display: block; margin: 0 auto 30rpx; }
.qr-hint { font-size: 24rpx; color: #999; display: block; margin-bottom: 20rpx; }
.qr-close { font-size: 26rpx; color: #1890ff; display: block; }
.qr-box { background: #fff; border-radius: 20rpx; padding: 40rpx; width: 560rpx; }
.qr-title { font-size: 30rpx; font-weight: 600; color: #333; margin-bottom: 24rpx; display: block; text-align: center; }
.c-input { border: 2rpx solid #e8e8e8; border-radius: 10rpx; padding: 16rpx; font-size: 26rpx; margin-bottom: 16rpx; width: 100%; box-sizing: border-box; }
.c-area { border: 2rpx solid #e8e8e8; border-radius: 10rpx; padding: 16rpx; font-size: 26rpx; margin-bottom: 16rpx; width: 100%; box-sizing: border-box; height: 120rpx; }
.c-submit { width: 100%; height: 80rpx; background: #1890ff; color: #fff; border: none; border-radius: 10rpx; font-size: 28rpx; margin-bottom: 12rpx; }
.qr-close { font-size: 26rpx; color: #1890ff; display: block; text-align: center; }
</style>
+4
View File
@@ -259,6 +259,10 @@ export const paymentApi = {
request('/payment/create-order', 'POST', { plan, pay_type: payType }),
}
export const leadApi = {
create: (data) => request('/leads', 'POST', data),
}
export const creditApi = {
balance: () => request('/credits/balance'),
history: (page = 1, size = 20) => request(`/credits/history?page=${page}&size=${size}`),
+2
View File
@@ -135,6 +135,8 @@ export function subscribeCreditPlan(planId, payType = 'alipay') {
}
export function cancelCreditSubscription() { return http.post('/credits/cancel-subscription') }
export function submitLead(data) { return http.post('/leads', data) }
export function startAgentPipeline(data) { return http.post('/agent/start', data, { timeout: 300000 }) }
export function listAgentPipelines(params) { return http.get('/agent/pipelines', { params }) }
export function getAgentPipeline(id) { return http.get(`/agent/${id}`) }
+21 -121
View File
@@ -9,51 +9,15 @@
<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 class="modal-summary">
<div class="summary-row"><strong>免费试用</strong><span>30 积分 + 每日 1000 字翻译零门槛体验</span></div>
<div class="summary-row"><strong>私有化部署</strong><span>独立部署数据私有不限账号年付授权</span></div>
<div class="summary-row"><strong>买断源码</strong><span>完整源码可二次开发永久授权</span></div>
</div>
<div class="modal-foot">
<span class="hint">订阅后网页端浏览器插件Skills 通用</span>
<el-button text size="small" @click="goCreditsPage">购买积分包低至 ¥2.9</el-button>
<span class="hint">网页端浏览器插件Skills 通用</span>
<el-button type="primary" size="small" @click="goUpgradePage">查看完整方案</el-button>
</div>
</div>
</div>
@@ -62,10 +26,8 @@
</template>
<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { ref, 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 },
@@ -76,93 +38,26 @@ const props = defineProps({
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)
}
function goUpgradePage() {
close()
router.push('/workspace/upgrade')
}
// 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()
})
watch(() => props.visible, () => {})
onMounted(() => {
window.addEventListener('trademate:upgrade', onUpgradeEvent)
if (props.visible) loadPlans()
})
onUnmounted(() => {
@@ -195,10 +90,15 @@ onUnmounted(() => {
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;
.modal-summary {
padding: 16px 24px; display: flex; flex-direction: column; gap: 10px;
}
.summary-row {
display: flex; justify-content: space-between; gap: 12px;
font-size: 13px; color: #475569; padding: 8px 12px;
background: #f8faff; border-radius: 8px;
}
.summary-row strong { color: #1e293b; white-space: nowrap; }
.plan-card {
flex: 1; border: 1px solid #e5e7eb; border-radius: 12px;
padding: 16px; text-align: center; position: relative;
+1
View File
@@ -22,6 +22,7 @@
@select="showMobileMenu = false"
>
<el-menu-item index="/workspace"><el-icon><Odometer /></el-icon><span>{{ $t('nav.home') }}</span></el-menu-item>
<el-menu-item index="/workspace/agent"><el-icon><MagicStick /></el-icon><span>{{ $t('nav.agent') || 'AI数字员工' }}</span></el-menu-item>
<el-menu-item index="/workspace/customers"><el-icon><User /></el-icon><span>{{ $t('nav.customers') }}</span></el-menu-item>
<el-menu-item index="/workspace/biz"><el-icon><Goods /></el-icon><span>{{ $t('nav.biz') }}</span></el-menu-item>
<el-menu-item index="/workspace/analytics"><el-icon><DataAnalysis /></el-icon><span>{{ $t('nav.analytics') }}</span></el-menu-item>
+2 -1
View File
@@ -13,6 +13,7 @@ const routes = [
{ path: 'customers', name: 'Customers', component: () => import('@/views/WorkspaceCustomer.vue'), meta: { title: '客户工作台' } },
{ path: 'biz', name: 'Biz', component: () => import('@/views/WorkspaceBiz.vue'), meta: { title: '业务工作台' } },
{ path: 'analytics', name: 'Analytics', component: () => import('@/views/Analytics.vue'), meta: { title: '数据分析' } },
{ path: 'agent', name: 'Agent', component: () => import('@/views/Agent.vue'), meta: { title: 'AI数字员工' } },
{ path: 'team', name: 'Team', component: () => import('@/views/Team.vue'), meta: { title: '团队协作' } },
{ path: 'profile', name: 'Profile', component: () => import('@/views/Profile.vue'), meta: { title: '个人中心' } },
{ path: 'profile/credits', name: 'Credits', component: () => import('@/views/Credits.vue'), meta: { title: '购买次数' } },
@@ -35,7 +36,7 @@ const routes = [
{ path: '/invoice', redirect: '/workspace/profile/invoice' },
{ path: '/notifications', redirect: '/workspace/profile/notifications' },
{ path: '/feedback', redirect: '/workspace/profile/feedback' },
{ path: '/agent', redirect: '/workspace' },
{ path: '/agent', redirect: '/workspace/agent' },
{ path: '/discovery', redirect: '/workspace/customers' },
{ path: '/followup', redirect: '/workspace/customers' },
{ path: '/marketing', redirect: '/workspace/biz' },
+97 -240
View File
@@ -1,254 +1,134 @@
<template>
<div class="upgrade-page">
<div class="page-head">
<h1>选择适合你的套餐</h1>
<p class="page-sub">订阅后所有产品线通用 网页工作台浏览器插件Agent Skills</p>
<div class="billing-toggle">
<el-radio-group v-model="billingPeriod" size="small">
<el-radio-button value="monthly">月付</el-radio-button>
<el-radio-button value="yearly">年付 <span class="save-tag" v-if="billingPeriod === 'yearly'"> 2 个月</span></el-radio-button>
</el-radio-group>
</div>
<h1>选择适合你的方案</h1>
<p class="page-sub">TradeMate 提供免费试用私有化部署与源码买断三种方式按需选择</p>
</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>
<!-- Free Trial -->
<div class="plan-card free">
<div class="plan-badge">推荐体验</div>
<div class="plan-name">免费试用</div>
<div class="plan-price free">¥0</div>
<div class="plan-credits">30 积分一次性+ 每日 1000 字免费翻译</div>
<ul class="plan-features">
<li>每日 1000 字免费翻译</li>
<li>基本功能体验</li>
<li>用完即止</li>
<li>AI 翻译 / 智能回复</li>
<li>客户发现 / 营销生成</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>
<el-button type="primary" class="plan-btn" @click="startTrial">免费使用</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>
<!-- Private Deployment -->
<div class="plan-card featured">
<div class="plan-badge">企业首选</div>
<div class="plan-name">私有化部署</div>
<div class="plan-price">
¥{{ billingPeriod === 'yearly' ? p.yearlyPrice : p.price }}
<small>/{{ billingPeriod === 'yearly' ? '年' : '月' }}</small>
¥{{ privatePlan.price }}<small>/{{ privatePlan.unit }}</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>
<div class="plan-credits">{{ privatePlan.credits }}</div>
<ul class="plan-features">
<li v-for="f in p.features" :key="f">{{ f }}</li>
<li>独立部署到你的服务器</li>
<li>数据完全私有安全合规</li>
<li>不限账号数与调用量</li>
<li>支持对接自有 AI 模型</li>
<li>一年技术支持与升级</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>
<el-button type="primary" class="plan-btn" @click="openContact('private')">申请私有化部署</el-button>
</div>
<!-- Source Buyout -->
<div class="plan-card">
<div class="plan-badge">一次买断</div>
<div class="plan-name">买断源码</div>
<div class="plan-price">
¥{{ buyoutPlan.price }}<small>/一次性</small>
</div>
<div class="plan-credits">{{ buyoutPlan.credits }}</div>
<ul class="plan-features">
<li>完整前端 + 后端源码</li>
<li>可自由二次开发</li>
<li>永久授权无后续费用</li>
<li>社区与文档支持</li>
</ul>
<el-button type="primary" class="plan-btn" @click="openContact('buyout')">咨询源码买断</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>
</el-card>
<!-- Purchase Dialog -->
<el-dialog v-model="payDialog.visible" title="选择支付方式" width="360px">
<p style="margin-bottom:12px;text-align:center" v-if="payDialog.type === 'subscription'">
订阅 <strong>{{ payDialog.plan?.name }}</strong>
({{ payDialog.plan?.credits }} 积分/)
</p>
<p style="margin-bottom:12px;text-align:center" v-else>
购买 <strong>{{ payDialog.pkg?.name }}</strong> ({{ payDialog.pkg?.credits }} 积分)
</p>
<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>
<!-- Contact dialog for enterprise offerings -->
<el-dialog v-model="contact.visible" :title="contactTitle" width="420px">
<el-form :model="contact.form" label-width="80px">
<el-form-item label="称呼" required>
<el-input v-model="contact.form.name" placeholder="您的称呼" />
</el-form-item>
<el-form-item label="公司">
<el-input v-model="contact.form.company" placeholder="公司名称(选填)" />
</el-form-item>
<el-form-item label="手机号" required>
<el-input v-model="contact.form.phone" placeholder="用于商务联系" />
</el-form-item>
<el-form-item label="需求">
<el-input v-model="contact.form.message" type="textarea" :rows="3" placeholder="部署规模 / 定制需求(选填)" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="payDialog.visible = false">取消</el-button>
<el-button type="primary" @click="confirmPay" :loading="payDialog.loading">确认支付</el-button>
<el-button @click="contact.visible = false">取消</el-button>
<el-button type="primary" :loading="contact.loading" @click="submitContact">提交申请</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
getSubscriptionPlans, getCreditPackages, getCreditBalance,
subscribeCreditPlan, purchaseCreditPackage,
} from '@/api'
import { submitLead } from '@/api'
const billingPeriod = ref('monthly')
const router = useRouter()
const loading = ref(false)
const plans = ref([])
const packages = ref([])
const currentPlan = ref('free')
const payingId = ref(null)
const payDialog = ref({
const PRICING = {
private: { price: '39,800', unit: '年', credits: '不限账号 · 不限调用' },
buyout: { price: '98,000', unit: '一次性', credits: '永久授权 · 可二开' },
}
const privatePlan = ref(PRICING.private)
const buyoutPlan = ref(PRICING.buyout)
const contact = ref({
visible: false,
type: 'subscription', // 'subscription' | 'package'
plan: null,
pkg: null,
payType: 'alipay',
type: 'private',
loading: false,
form: { name: '', company: '', phone: '', message: '' },
})
const contactTitle = computed(() => contact.value.type === 'private' ? '申请私有化部署' : '咨询源码买断')
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 保障'] },
function startTrial() {
router.push('/workspace')
}
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 || [],
function openContact(type) {
contact.value.type = type
contact.value.form = { name: '', company: '', phone: '', message: '' }
contact.value.visible = true
}
}).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
async function submitContact() {
const f = contact.value.form
if (!f.name.trim() || !f.phone.trim()) {
ElMessage.warning('请填写称呼与手机号')
return
}
contact.value.loading = 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,
}
}
async function confirmPay() {
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
await submitLead({ type: contact.value.type, ...f })
ElMessage.success('提交成功,商务会尽快联系您')
contact.value.visible = false
} catch (e) {
ElMessage.error(e?.detail || e?.message || '支付失败')
ElMessage.error(e?.detail || e?.message || '提交失败')
}
d.loading = false
contact.value.loading = false
}
function buyPackage(pkg) {
payDialog.value = {
visible: true,
type: 'package',
plan: null,
pkg,
payType: 'alipay',
loading: false,
}
}
onMounted(loadData)
</script>
<style scoped>
@@ -256,9 +136,7 @@ onMounted(loadData)
.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; }
.plans-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }
.plan-card {
background: #fff; border: 1px solid #e5e7eb; border-radius: 16px;
padding: 24px 20px; text-align: center; position: relative;
@@ -266,40 +144,19 @@ onMounted(loadData)
}
.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-card.free .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 { font-size: 30px; font-weight: 800; color: #2563eb; margin: 8px 0 2px; }
.plan-price.free { color: #64748b; font-size: 22px; }
.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>