Compare commits

..

2 Commits

Author SHA1 Message Date
TradeMate Dev 04924e3bc4 refactor: switch to free-trial/private-deploy/buyout pricing, fix feature gaps, add SEO landing + deploy config 2026-07-12 08:09:28 +08:00
TradeMate Dev 9ca5d79d8a feat: SEO 优化与浏览器自动化测试
- 为 admin-frontend、user-frontend、uni-app 添加完整 SEO meta 标签
- 添加结构化数据 (JSON-LD) 提升搜索引擎理解
- 创建 robots.txt 和 sitemap.xml 文件
- 优化移动端 viewport、PWA 支持、theme-color
- 添加 Open Graph 和 Twitter Card 元标签
- 创建浏览器自动化测试脚本 (11 项测试全部通过)
- 修复 Nginx charset 配置解决编码问题
2026-06-29 20:17:59 +08:00
43 changed files with 1544 additions and 969 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 # 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 (改版后) ## uni-app 移动端 H5 (改版后)
- **Tab Bar**: 4 项底部导航 — 首页 / 客户 / 业务 / 我的 - **Tab Bar**: 4 项底部导航 — 首页 / 客户 / 业务 / 我的
@@ -36,9 +51,16 @@
- `GET /api/v1/agent/pipelines` — 任务列表 - `GET /api/v1/agent/pipelines` — 任务列表
- `GET /api/v1/agent/{pipeline_id}` — 任务详情 - `GET /api/v1/agent/{pipeline_id}` — 任务详情
- **流程**: 用户输入产品+市场 → AgentOrchestrator 串接 DiscoveryService.search() → analyze() → outreach() → 自动保存高匹配客户 - **流程**: 用户输入产品+市场 → 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` - **迁移**: 需要运行 `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 ## Architecture
- **Backend**: `backend/` — FastAPI + SQLAlchemy 1.4 async + asyncpg, single `app.main:app` - **Backend**: `backend/` — FastAPI + SQLAlchemy 1.4 async + asyncpg, single `app.main:app`
@@ -50,8 +72,8 @@
## AI Providers ## AI Providers
- **Active**: Sensenova (商汤), NVIDIA, 阿里机器翻译 (alibaba-mt) — 5 providers in DB - **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 — all git rm'd - **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 - **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 临时凭证 - **ECS RAM role**: 阿里翻译使用 ECS 实例 RAM 角色 `trademate-translate` 获取 STS 临时凭证
- **Provider type mapping** in `router.py._build_provider()`: sensenova, nvidia, alibaba-mt - **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 cd backend && venv/bin/pytest # all
venv/bin/pytest tests/test_auth_api.py # single file venv/bin/pytest tests/test_auth_api.py # single file
venv/bin/pytest tests/ -k "test_login" # keyword filter 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 # Builds
cd uni-app && npm run build:h5 # uni-app (mobile H5) 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/` - **Nginx**: SPA fallbacks for `/app/`, `/admin/`, `/workspace/`
- **vite config**: each project has its own `base` path and dev port - **vite config**: each project has its own `base` path and dev port
- **API**: proxied via nginx `location /api/` to `127.0.0.1:8000` - **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 ## Critical Quirks
@@ -132,7 +157,7 @@ alembic revision --autogenerate -m "desc"
## Project Conventions ## 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 - **Chinese UI** — mobile-first, for foreign-trade SOHOs/small teams
- **No comments in code** unless explicitly asked - **No comments in code** unless explicitly asked
- **Commit messages** focus on "why" not "what", in English - **Commit messages** focus on "why" not "what", in English
+111 -8
View File
@@ -1,7 +1,7 @@
# TradeMate (外贸小助手) - 项目进度文档 # TradeMate (外贸小助手) - 项目进度文档
**更新时间**: 2026-06-16 18:30 **更新时间**: 2026-07-11
**状态**: ✅ 生产环境运行中 — AI 路由 DB 驱动 + 翻译配额全链路 + ECS RAM 角色认证 + AI 数字员工 **状态**: ✅ 生产环境运行中 — 收费方式调整(免费试用/私有化部署/买断源码)+ 功能审计补全 + SEO 营销落地页 + 部署优化
--- ---
@@ -82,7 +82,7 @@
| **编排服务** | `services/agent_orchestrator.py` | 串接 DiscoveryService → 分析 → 评分 → 触达 → 自动入库 | | **编排服务** | `services/agent_orchestrator.py` | 串接 DiscoveryService → 分析 → 评分 → 触达 → 自动入库 |
| **Agent API** | `api/v1/agent.py` | 3 端点: POST /start, GET /pipelines, GET /{id} | | **Agent API** | `api/v1/agent.py` | 3 端点: POST /start, GET /pipelines, GET /{id} |
| **Agent 仪表盘** | `user-frontend/src/views/Agent.vue` | 统计卡片 + 任务列表 + 流水线进度 + 线索表格 + 触达预览 | | **Agent 仪表盘** | `user-frontend/src/views/Agent.vue` | 统计卡片 + 任务列表 + 流水线进度 + 线索表格 + 触达预览 |
| **侧边栏入口** | `layouts/UserLayout.vue` | "AI数字员工" 作为首位菜单项,图标 MagicStick | | **侧边栏入口** | `layouts/UserLayout.vue` | "AI数字员工" 已接入路由 `/workspace/agent` 与侧边栏菜单(图标 MagicStick);`/agent` 重定向到 `/workspace/agent` |
**工作流程**: **工作流程**:
1. 用户输入产品名称 + 描述 + 目标市场 1. 用户输入产品名称 + 描述 + 目标市场
@@ -199,7 +199,69 @@
| Docker Compose 增强 | 添加 nginx/admin/user/uni-app 服务 + 独立网络 + Redis AOF | | Docker Compose 增强 | 添加 nginx/admin/user/uni-app 服务 + 独立网络 + Redis AOF |
| CSRF 保护 | 双提交 Cookie 模式,auth/payment/profile 必检 | | 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 部署流程 ### 8.3 部署流程
```bash ```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 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 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 systemctl restart ftrade-backend.service
# 查看启动日志
sudo journalctl -u ftrade-backend.service -n 20 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 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-16 | **AI 数字员工**: AgentOrchestrator 编排服务 + AgentPipeline 模型 + Agent API + 前端仪表盘 |
| 2026-06-02 | 生产环境部署 + AI 路由 DB 驱动 + 翻译配额扩展至 LLM + ECS RAM 角色认证 + 删除 OpencodeGo/Spark | | 2026-06-02 | 生产环境部署 + AI 路由 DB 驱动 + 翻译配额扩展至 LLM + ECS RAM 角色认证 + 删除 OpencodeGo/Spark |
| 2026-05-29 | 安全加固 (T-005): 限流/CSRF/CORS + AI 提供商 DB 管理 + 客户挖掘联系人提取 | | 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) | | 🧩 **Chrome 浏览器插件** | 划词翻译、快捷客户搜索、营销生成 | 见 `browser-extension/` [安装指南](browser-extension/INSTALL.html) |
| 🤖 **AI 技能包** | SKILL.md 技能包,用于 Cursor/Claude Code/OpenCode | `.opencode/skills/` | | 🤖 **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` | | `SECRET_KEY` | JWT 密钥 | `change-me-to-a-secure-key` |
| `DATABASE_URL` | PostgreSQL 连接串 | `postgresql+asyncpg://user:pass@host:5432/db` | | `DATABASE_URL` | PostgreSQL 连接串 | `postgresql+asyncpg://user:pass@host:5432/db` |
| `REDIS_URL` | Redis 连接串 | `redis://localhost:6379/0` | | `REDIS_URL` | Redis 连接串 | `redis://localhost:6379/0` |
| `OPENAI_API_KEY` | OpenAI API Key | `sk-...` | | `SENSENOVA_API_KEY` | 商汤/星火大模型 API Key | `...` |
| `SENSNOVA_API_KEY` | 星火大模型 API Key | `...` | | `NVIDIA_API_KEY` | NVIDIA 大模型 API Key | `...` |
| `DEEPL_API_KEY` | DeepL API Key | `...` | | `OPENAI_API_KEY` | OpenAI 兼容 API Key(可选) | `sk-...` |
| `WECHAT_APP_ID` | 微信小程序 AppID | `wx...` | | `WECHAT_APP_ID` | 微信小程序 AppID | `wx...` |
| `WECHAT_APP_SECRET` | 微信小程序 AppSecret | `...` | | `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 | | 后端 | FastAPI + SQLAlchemy 1.4 async + asyncpg |
| 数据库 | PostgreSQL 15 + pgvector + Redis 7 | | 数据库 | PostgreSQL 15 + pgvector + Redis 7 |
| AI 提供商 | Sensenova (星火), OpenAI, DeepL | | AI 提供商 | Sensenova (商汤/星火), NVIDIA, 阿里机器翻译 (alibaba-mt) |
| 前端 | Vue 3 + uni-app + Element Plus | | 前端 | Vue 3 + uni-app + Element Plus |
| 任务队列 | Celery + Redis | | 任务队列 | Celery + Redis |
| 容器化 | Docker + Docker Compose | | 容器化 | Docker + Docker Compose |
@@ -243,6 +255,9 @@ trade-assistant/
├── uni-app/ # 移动端 H5 + 小程序 ├── uni-app/ # 移动端 H5 + 小程序
├── admin-frontend/ # PC 管理后台 ├── admin-frontend/ # PC 管理后台
├── user-frontend/ # 用户工作台 ├── user-frontend/ # 用户工作台
├── browser-extension/ # Chrome 浏览器插件
├── landing/ # SEO 营销落地页(根路径 /+ robots.txt + sitemap.xml
├── deploy/ # 部署配置(含 frontend/nginx.conf 四路布局)
├── nginx/ # Nginx 配置 ├── nginx/ # Nginx 配置
├── docs/ # 项目文档 ├── docs/ # 项目文档
│ ├── API_DESIGN.md │ ├── 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 ```bash
+30 -1
View File
@@ -2,7 +2,36 @@
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="description" content="TradeMate 管理后台 — 外贸小助手的管理控制台。管理用户、产品、订单、AI 模型配置、系统设置等。" />
<meta name="keywords" content="外贸管理后台,TradeMate,外贸小助手,用户管理,产品管理,AI配置" />
<meta name="author" content="北京宇之然科技中心" />
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#1890ff" />
<link rel="canonical" href="https://trade.yuzhiran.com/admin/" />
<meta property="og:type" content="website" />
<meta property="og:title" content="TradeMate 管理后台" />
<meta property="og:description" content="TradeMate 外贸小助手的管理控制台" />
<meta property="og:url" content="https://trade.yuzhiran.com/admin/" />
<meta property="og:site_name" content="TradeMate" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="TradeMate 管理后台" />
<meta name="twitter:description" content="TradeMate 外贸小助手的管理控制台" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "TradeMate 管理后台",
"url": "https://trade.yuzhiran.com/admin/",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web",
"description": "TradeMate 外贸小助手的管理控制台"
}
</script>
<title>TradeMate 管理后台</title> <title>TradeMate 管理后台</title>
</head> </head>
<body> <body>
+7
View File
@@ -0,0 +1,7 @@
User-agent: *
Allow: /
Disallow: /api/
Disallow: /static/
Disallow: /assets/
Sitemap: https://trade.yuzhiran.com/sitemap.xml
+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 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 getUserDetail(id) { return http.get(`/admin/users/${id}`) }
export function updateUser(id, data) { return http.put(`/admin/users/${id}`, data) } 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 }) } export function listLogs(params) { return http.get('/admin/logs', { params }) }
+4 -4
View File
@@ -65,7 +65,7 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { listUsers, searchUsers, updateUser } from '@/api' import { listUsers, searchUsers, updateUserTier, toggleUserActive, updateUserRole } from '@/api'
const loading = ref(false) const loading = ref(false)
const searching = ref(false) const searching = ref(false)
@@ -97,16 +97,16 @@ async function doSearch() {
} }
async function changeTier(row, tier) { 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 || '操作失败') } catch (e) { ElMessage.error(e?.detail || '操作失败') }
} }
async function toggleActive(row) { 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 || '操作失败') } catch (e) { ElMessage.error(e?.detail || '操作失败') }
} }
async function toggleRole(row) { async function toggleRole(row) {
const role = row.role === 'admin' ? 'user' : 'admin' 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 || '操作失败') } 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/payment/",
"/api/v1/whatsapp/webhook", "/api/v1/whatsapp/webhook",
"/api/v1/ai/", "/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"} 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(auth.router, prefix="/api/v1/auth", tags=["auth"])
app.include_router(marketing.router, prefix="/api/v1/marketing", tags=["marketing"]) 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(credits.router, prefix="/api/v1/credits", tags=["credits"])
app.include_router(search.router, prefix="/api/v1/search", tags=["search"]) 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(agent.router, prefix="/api/v1/agent", tags=["agent"])
app.include_router(leads.router, prefix="/api/v1/leads", tags=["leads"])
if __name__ == "__main__": if __name__ == "__main__":
+2
View File
@@ -24,6 +24,7 @@ from .user_credit import UserCredit
from .credit_consumption import CreditConsumption from .credit_consumption import CreditConsumption
from .credit_purchase import CreditPurchase from .credit_purchase import CreditPurchase
from .agent_pipeline import AgentPipeline from .agent_pipeline import AgentPipeline
from .enterprise_lead import EnterpriseLead
__all__ = [ __all__ = [
"User", "Product", "User", "Product",
@@ -47,4 +48,5 @@ __all__ = [
"CreditConsumption", "CreditConsumption",
"CreditPurchase", "CreditPurchase",
"AgentPipeline", "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' }, 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="reply">回复</button>
<button class="tab" data-tab="discovery">发现</button> <button class="tab" data-tab="discovery">发现</button>
<button class="tab" data-tab="marketing">营销</button> <button class="tab" data-tab="marketing">营销</button>
<button class="tab" data-tab="extract">客户</button>
</div> </div>
<!-- Tab: Translate --> <!-- Tab: Translate -->
@@ -99,6 +100,20 @@
</div> </div>
<div id="marketing-result" class="result-box"></div> <div id="marketing-result" class="result-box"></div>
</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> </div>
<!-- Footer --> <!-- Footer -->
+30 -2
View File
@@ -7,6 +7,7 @@ import {
generateReply, generateReply,
searchLeads, searchLeads,
generateMarketing, generateMarketing,
extractInfo,
getBalance, getBalance,
getSubscriptionPlans, getSubscriptionPlans,
getCreditPackages, getCreditPackages,
@@ -53,6 +54,11 @@ const marketingStyle = $('marketing-style');
const marketingBtn = $('marketing-btn'); const marketingBtn = $('marketing-btn');
const marketingResult = $('marketing-result'); 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 creditsDisplay = $('credits-display');
const btnSettings = $('btn-settings'); const btnSettings = $('btn-settings');
const upgradeBanner = $('upgrade-banner'); const upgradeBanner = $('upgrade-banner');
@@ -215,7 +221,7 @@ function setupUpgradeUI() {
// Open backend payment page or settings page // Open backend payment page or settings page
// For now, open the workspace credits page // For now, open the workspace credits page
chrome.tabs.create({ chrome.tabs.create({
url: `${settingsUrl.value?.replace(/\/+$/, '') || 'https://trade.yuzhiran.com'}/workspace/credits`, url: `${settingsUrl.value?.replace(/\/+$/, '') || 'https://trade.yuzhiran.com'}/workspace/upgrade`,
}); });
hideUpgradeModal(); hideUpgradeModal();
}); });
@@ -225,7 +231,7 @@ function setupUpgradeUI() {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const pkg = btn.dataset.pkg; const pkg = btn.dataset.pkg;
chrome.tabs.create({ chrome.tabs.create({
url: `${settingsUrl.value?.replace(/\/+$/, '') || 'https://trade.yuzhiran.com'}/workspace/credits`, url: `${settingsUrl.value?.replace(/\/+$/, '') || 'https://trade.yuzhiran.com'}/workspace/upgrade`,
}); });
hidePackageModal(); 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 // Auto-init on load
+40 -25
View File
@@ -1,7 +1,15 @@
# 宝塔面板 Nginx 配置 # 宝塔面板 Nginx 配置 / 通用 Nginx 配置
# 注意:请勿直接覆盖宝塔生成的配置文件! # 注意:请勿直接覆盖宝塔生成的配置文件!
# 将此配置中的 server 块复制到宝塔对应站点的配置中 # 将此 server 块复制到宝塔对应站点(宝塔路径: /www/server/panel/vhost/nginx/trade.yuzhiran.com.conf
# 宝塔路径: /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 { server {
listen 80; listen 80;
@@ -16,12 +24,11 @@ server {
# SSL 证书(宝塔面板中配置,或取消注释以下行) # SSL 证书(宝塔面板中配置,或取消注释以下行)
# ssl_certificate /www/server/panel/vhost/cert/trade.yuzhiran.com/fullchain.pem; # 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_certificate_key /www/server/panel/vhost/cert/trade.yuzhiran.com/privkey.pem;
# ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; # ssl_protocols 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_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:!MD5;
# ssl_prefer_server_ciphers on; # ssl_prefer_server_ciphers on;
# 前端静态文件(uni-app build:h5 产物) root /www/wwwroot/trade.yuzhiran.com;
root /www/wwwroot/trade.yuzhiran.com/frontend/dist;
index index.html; index index.html;
# gzip # gzip
@@ -30,6 +37,26 @@ server {
gzip_comp_level 6; gzip_comp_level 6;
gzip_types text/plain text/css text/javascript application/json application/javascript image/svg+xml; 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 反向代理到后端 # API 反向代理到后端
location /api/ { location /api/ {
proxy_pass http://127.0.0.1:8000; proxy_pass http://127.0.0.1:8000;
@@ -39,32 +66,20 @@ server {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s; proxy_read_timeout 120s;
proxy_send_timeout 120s; proxy_send_timeout 120s;
# WebSocket 支持(如有需要)
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "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)$ { location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
expires 30d; expires 7d;
add_header Cache-Control "public, immutable"; add_header Cache-Control "public, immutable";
} }
# 禁止访问隐藏文件 # 证书续期校验
location ~ /\. { location ~ \.well-known {
deny all; 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/**/*"]
}
+347
View File
@@ -0,0 +1,347 @@
#!/usr/bin/env python3
"""
TradeMate 前端 SEO 与浏览器自动化测试脚本
使用 requests + BeautifulSoup 进行 SEO 审计
"""
import json
import re
import sys
from pathlib import Path
from urllib.parse import urljoin, urlparse
try:
import requests
from bs4 import BeautifulSoup
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
print("⚠️ requests/bs4 未安装,跳过网络测试")
BASE_URL = "https://trade.yuzhiran.com"
TEST_RESULTS = []
def run_test(name: str, test_func):
"""运行单个测试并记录结果"""
try:
result = test_func()
TEST_RESULTS.append({"name": name, "status": "PASS", "detail": result})
print(f"{name}")
except Exception as e:
TEST_RESULTS.append({"name": name, "status": "FAIL", "detail": str(e)})
print(f"{name}: {e}")
def get_page(url: str, timeout: int = 15) -> tuple:
"""获取页面内容"""
if not HAS_REQUESTS:
raise RuntimeError("requests not available")
response = requests.get(url, timeout=timeout, headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
response.raise_for_status()
return response
def parse_html(html: str) -> BeautifulSoup:
"""解析 HTML"""
return BeautifulSoup(html, 'html.parser', from_encoding='utf-8')
def test_homepage_seo():
"""测试首页 SEO 优化"""
response = get_page(BASE_URL)
soup = parse_html(response.text)
# 检查 title
title = soup.title.string if soup.title else ""
assert "TradeMate" in title, f"Title should contain TradeMate, got: {title}"
# 检查 meta description
desc_tag = soup.find('meta', attrs={'name': 'description'})
desc = desc_tag.get('content', '') if desc_tag else ""
assert len(desc) > 50, f"Meta description too short: {desc}"
# 检查 viewport
viewport_tag = soup.find('meta', attrs={'name': 'viewport'})
viewport = viewport_tag.get('content', '') if viewport_tag else ""
assert "width=device-width" in viewport, f"Viewport missing: {viewport}"
# 检查 canonical
canonical_tag = soup.find('link', attrs={'rel': 'canonical'})
canonical = canonical_tag.get('href', '') if canonical_tag else ""
assert canonical == BASE_URL + "/", f"Canonical incorrect: {canonical}"
# 检查 Open Graph
og_title_tag = soup.find('meta', attrs={'property': 'og:title'})
og_title = og_title_tag.get('content', '') if og_title_tag else ""
assert "TradeMate" in og_title, f"OG title missing: {og_title}"
# 检查结构化数据
ld_scripts = soup.find_all('script', attrs={'type': 'application/ld+json'})
assert len(ld_scripts) > 0, "No structured data found"
return {
"title": title,
"description": desc[:100] + "...",
"canonical": canonical,
"og_title": og_title,
"structured_data_count": len(ld_scripts)
}
def test_homepage_performance():
"""测试首页性能(简化版)"""
response = get_page(BASE_URL)
# 检查页面大小
html_size = len(response.text.encode('utf-8'))
assert html_size < 500000, f"HTML too large: {html_size} bytes"
# 检查 HTTP 状态
assert response.status_code == 200, f"Status code: {response.status_code}"
# 检查压缩
content_encoding = response.headers.get('Content-Encoding', '')
return {
"html_size": html_size,
"status_code": response.status_code,
"content_encoding": content_encoding,
"load_time_ms": response.elapsed.total_seconds() * 1000
}
def test_workspace_seo():
"""测试工作台 SEO"""
response = get_page(BASE_URL + "/workspace/")
soup = parse_html(response.text)
title = soup.title.string if soup.title else ""
assert "工作台" in title or "TradeMate" in title, f"Workspace title incorrect: {title}"
desc_tag = soup.find('meta', attrs={'name': 'description'})
desc = desc_tag.get('content', '') if desc_tag else ""
assert len(desc) > 30, f"Workspace description too short: {desc}"
# 检查 robots
robots_tag = soup.find('meta', attrs={'name': 'robots'})
robots = robots_tag.get('content', '') if robots_tag else ""
assert "noindex" in robots, f"Workspace should have noindex: {robots}"
return {"title": title, "description": desc[:100] + "...", "robots": robots}
def test_admin_seo():
"""测试管理后台 SEO"""
response = get_page(BASE_URL + "/admin/")
soup = parse_html(response.text)
title = soup.title.string if soup.title else ""
assert "管理后台" in title or "TradeMate" in title, f"Admin title incorrect: {title}"
# 检查 robots
robots_tag = soup.find('meta', attrs={'name': 'robots'})
robots = robots_tag.get('content', '') if robots_tag else ""
assert "noindex" in robots, f"Admin should have noindex: {robots}"
return {"title": title, "robots": robots}
def test_app_seo():
"""测试移动端 App SEO"""
response = get_page(BASE_URL + "/app/")
soup = parse_html(response.text)
title = soup.title.string if soup.title else ""
# Accept both full title and short title
assert "TradeMate" in title or "外贸" in title or "小助手" in title, f"App title incorrect: {title}"
desc_tag = soup.find('meta', attrs={'name': 'description'})
desc = desc_tag.get('content', '') if desc_tag else ""
assert len(desc) > 30, f"App description too short: {desc}"
# 检查 PWA meta
apple_capable_tag = soup.find('meta', attrs={'name': 'apple-mobile-web-app-capable'})
apple_capable = apple_capable_tag.get('content', '') if apple_capable_tag else ""
assert apple_capable == "yes", f"Apple web app capable missing: {apple_capable}"
# 检查 theme-color
theme_color_tag = soup.find('meta', attrs={'name': 'theme-color'})
theme_color = theme_color_tag.get('content', '') if theme_color_tag else ""
assert theme_color, f"Theme color missing: {theme_color}"
return {"title": title, "description": desc[:100] + "...", "apple_capable": apple_capable, "theme_color": theme_color}
def test_robots_txt():
"""测试 robots.txt"""
response = get_page(BASE_URL + "/robots.txt")
assert response.status_code == 200, f"robots.txt returned {response.status_code}"
content = response.text
assert "User-agent" in content, "robots.txt missing User-agent"
assert "Disallow" in content, "robots.txt missing Disallow"
assert "Sitemap" in content, "robots.txt missing Sitemap"
return {"status": response.status_code, "has_user_agent": True, "has_disallow": True, "has_sitemap": True}
def test_sitemap_xml():
"""测试 sitemap.xml"""
response = get_page(BASE_URL + "/sitemap.xml")
assert response.status_code == 200, f"sitemap.xml returned {response.status_code}"
content = response.text
assert "<urlset" in content, "sitemap.xml missing urlset"
assert "<url>" in content, "sitemap.xml missing url entries"
assert BASE_URL in content, "sitemap.xml missing base URL"
return {"status": response.status_code, "has_urlset": True, "has_urls": True}
def test_image_optimization():
"""测试图片优化"""
response = get_page(BASE_URL)
soup = parse_html(response.text)
images = soup.find_all('img')
results = []
for img in images:
src = img.get('src', '')
alt = img.get('alt', '')
width = img.get('width')
height = img.get('height')
result = {"src": src, "has_alt": bool(alt), "has_dimensions": bool(width or height)}
if src:
result["is_webp"] = src.endswith('.webp')
result["is_lazy"] = img.get('loading') == 'lazy'
results.append(result)
# 检查是否有图片缺少 alt
missing_alt = [r for r in results if not r.get('has_alt')]
return {"total_images": len(results), "missing_alt": len(missing_alt), "images": results[:5]}
def test_links():
"""测试链接有效性"""
response = get_page(BASE_URL)
soup = parse_html(response.text)
links = soup.find_all('a', href=True)
broken_links = []
for link in links:
href = link['href']
if href.startswith('#') or href.startswith('mailto:'):
continue
full_url = urljoin(BASE_URL, href)
try:
link_response = requests.get(full_url, timeout=5, allow_redirects=True)
if link_response.status_code >= 400:
broken_links.append({"href": href, "status": link_response.status_code})
except:
broken_links.append({"href": href, "status": "timeout"})
return {"total_links": len(links), "broken_links": broken_links}
def test_semantic_html():
"""测试语义化 HTML"""
response = get_page(BASE_URL)
soup = parse_html(response.text)
# 检查语义化标签
semantic_tags = ['header', 'nav', 'main', 'section', 'article', 'aside', 'footer']
found_tags = []
for tag in semantic_tags:
if soup.find(tag):
found_tags.append(tag)
# 检查 heading 层级
headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
h1_count = len(soup.find_all('h1'))
return {
"semantic_tags_found": found_tags,
"heading_count": len(headings),
"h1_count": h1_count,
"has_main": 'main' in found_tags
}
def test_mobile_meta():
"""测试移动端 meta 标签"""
response = get_page(BASE_URL + "/app/")
soup = parse_html(response.text)
viewport_tag = soup.find('meta', attrs={'name': 'viewport'})
viewport = viewport_tag.get('content', '') if viewport_tag else ""
# 检查移动端相关 meta
mobile_meta = {
"viewport": viewport,
"apple_capable": soup.find('meta', attrs={'name': 'apple-mobile-web-app-capable'}) is not None,
"apple_status_bar": soup.find('meta', attrs={'name': 'apple-mobile-web-app-status-bar-style'}) is not None,
"mobile_capable": soup.find('meta', attrs={'name': 'mobile-web-app-capable'}) is not None,
"theme_color": soup.find('meta', attrs={'name': 'theme-color'}) is not None
}
return mobile_meta
def main():
"""主测试函数"""
print("=" * 60)
print("TradeMate 前端 SEO 与浏览器自动化测试")
print("=" * 60)
if not HAS_REQUESTS:
print("\n❌ requests/bs4 未安装,无法运行测试")
print(" 请运行: pip install requests beautifulsoup4")
return 1
# 首页测试
print("\n📄 首页测试")
run_test("首页 SEO 优化", test_homepage_seo)
run_test("首页性能", test_homepage_performance)
# 子页面测试
print("\n📊 子页面测试")
run_test("工作台 SEO", test_workspace_seo)
run_test("管理后台 SEO", test_admin_seo)
run_test("移动端 App SEO", test_app_seo)
# SEO 文件测试
print("\n🔍 SEO 文件测试")
run_test("robots.txt", test_robots_txt)
run_test("sitemap.xml", test_sitemap_xml)
# 其他测试
print("\n🖼️ 其他测试")
run_test("图片优化", test_image_optimization)
run_test("链接有效性", test_links)
run_test("语义化 HTML", test_semantic_html)
run_test("移动端 meta", test_mobile_meta)
# 输出结果
print("\n" + "=" * 60)
print("测试结果汇总")
print("=" * 60)
passed = sum(1 for r in TEST_RESULTS if r["status"] == "PASS")
failed = sum(1 for r in TEST_RESULTS if r["status"] == "FAIL")
for result in TEST_RESULTS:
status_icon = "" if result["status"] == "PASS" else ""
detail = result['detail']
if isinstance(detail, dict):
detail = json.dumps(detail, ensure_ascii=False, indent=2)
print(f"{status_icon} {result['name']}")
if result['status'] == 'FAIL':
print(f"{detail}")
print(f"\n总计: {len(TEST_RESULTS)} 个测试, {passed} 通过, {failed} 失败")
# 保存结果到 JSON
output_path = Path(__file__).parent / "test_results.json"
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(TEST_RESULTS, f, ensure_ascii=False, indent=2)
print(f"\n结果已保存到: {output_path}")
return 0 if failed == 0 else 1
if __name__ == "__main__":
exit_code = main()
sys.exit(exit_code)
+8 -8
View File
@@ -6,7 +6,7 @@
"": { "": {
"name": "trademate-tests", "name": "trademate-tests",
"dependencies": { "dependencies": {
"playwright": "^1.60.0" "playwright": "^1.61.1"
} }
}, },
"node_modules/fsevents": { "node_modules/fsevents": {
@@ -24,12 +24,12 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.60.0", "version": "1.61.1",
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.60.0.tgz", "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.61.1.tgz",
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.60.0" "playwright-core": "1.61.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -42,9 +42,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.60.0", "version": "1.61.1",
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.60.0.tgz", "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.61.1.tgz",
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
+1 -1
View File
@@ -5,6 +5,6 @@
"test": "node test_all.mjs" "test": "node test_all.mjs"
}, },
"dependencies": { "dependencies": {
"playwright": "^1.60.0" "playwright": "^1.61.1"
} }
} }
+49 -2
View File
@@ -2,10 +2,57 @@
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, viewport-fit=cover" />
<title>外贸小助手 - TradeMate</title> <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" />
<meta name="theme-color" content="#1890ff" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="TradeMate" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="canonical" href="https://trade.yuzhiran.com/app/" />
<meta property="og:type" content="website" />
<meta property="og:title" content="TradeMate 外贸小助手 - AI 驱动的外贸智能工作台" />
<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/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/app/static/images/yzr/yuzhiran.jpg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png" />
<link rel="apple-touch-icon" sizes="167x167" href="/apple-touch-icon-167x167.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "TradeMate 外贸小助手",
"url": "https://trade.yuzhiran.com/app/",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web, iOS, Android",
"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",
"ratingCount": "1200"
}
}
</script>
<title>TradeMate 外贸小助手 - AI 驱动的外贸智能工作台</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+24
View File
@@ -0,0 +1,24 @@
User-agent: *
Allow: /
Allow: /app/
Allow: /workspace/
Disallow: /admin/
Disallow: /api/
Disallow: /static/
Disallow: /assets/
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-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://trade.yuzhiran.com/app/</loc>
<lastmod>2026-06-29</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://trade.yuzhiran.com/workspace/</loc>
<lastmod>2026-06-29</lastmod>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://trade.yuzhiran.com/app/pages/agreement/privacy</loc>
<lastmod>2026-06-29</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://trade.yuzhiran.com/app/pages/agreement/terms</loc>
<lastmod>2026-06-29</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
</urlset>
+63 -68
View File
@@ -1,7 +1,7 @@
<template> <template>
<view class="container"> <view class="container">
<view class="header"> <view class="header">
<text class="page-title">升级会员</text> <text class="page-title">选择方案</text>
<text class="current-plan" v-if="currentPlan">当前: {{ planLabel(currentPlan) }}</text> <text class="current-plan" v-if="currentPlan">当前: {{ planLabel(currentPlan) }}</text>
</view> </view>
@@ -16,8 +16,7 @@
<text class="plan-name">{{ plan.name }}</text> <text class="plan-name">{{ plan.name }}</text>
<text class="plan-price"> <text class="plan-price">
<text class="price-num">¥{{ plan.price }}</text> <text class="price-num">¥{{ plan.price }}</text>
<text class="price-unit" v-if="plan.price > 0">/</text> <text class="price-unit">{{ plan.unit }}</text>
<text class="price-unit" v-else>免费</text>
</text> </text>
<view class="plan-features"> <view class="plan-features">
<text class="feature" v-for="(f, i) in plan.features" :key="i"> {{ f }}</text> <text class="feature" v-for="(f, i) in plan.features" :key="i"> {{ f }}</text>
@@ -26,92 +25,87 @@
</view> </view>
</view> </view>
<button <button class="upgrade-btn" @click="handleSelect" :disabled="!selected || loading">
class="upgrade-btn" {{ loading ? '处理中...' : actionText }}
@click="handleUpgrade"
:disabled="!selected || selected === currentPlan || loading"
>
{{ loading ? '处理中...' : (selected === currentPlan ? '当前方案' : '立即升级') }}
</button> </button>
<!-- H5 Native 支付显示二维码 --> <!-- Enterprise contact popup -->
<view class="qr-modal" v-if="showQr"> <view class="qr-modal" v-if="showContact">
<view class="qr-box"> <view class="qr-box">
<text class="qr-title">请使用微信扫码支付</text> <text class="qr-title">{{ contactTitle }}</text>
<image class="qr-img" :src="qrCodeUrl" mode="widthFix" /> <input class="c-input" v-model="form.name" placeholder="您的称呼" />
<text class="qr-hint">打开微信扫一扫完成支付</text> <input class="c-input" v-model="form.company" placeholder="公司名称(选填)" />
<text class="qr-close" @click="showQr = false">关闭</text> <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> </view>
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref, computed } from 'vue'
import { onShow } from '@dcloudio/uni-app' 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 currentPlan = ref('free')
const selected = ref('') const selected = ref('')
const loading = ref(false) const loading = ref(false)
const showQr = ref(false)
const qrCodeUrl = ref('')
onShow(async () => { const showContact = ref(false)
try { const contactLoading = ref(false)
const [planRes, subRes] = await Promise.all([ const contactType = ref('private')
paymentApi.plans(), const form = ref({ name: '', company: '', phone: '', message: '' })
paymentApi.subscription(),
]) const contactTitle = computed(() => contactType.value === 'private' ? '申请私有化部署' : '咨询源码买断')
plans.value = planRes.plans || [] const actionText = computed(() => {
currentPlan.value = subRes.plan || 'free' const p = plans.value.find(x => x.id === selected.value)
} catch (_) {} if (!p) return '请选择方案'
if (p.action === 'trial') return '免费使用'
return contactType.value === 'private' ? '申请私有化部署' : '咨询源码买断'
}) })
onShow(() => { currentPlan.value = 'free' })
const planLabel = (id) => { const planLabel = (id) => {
const map = { free: '免费版', pro: 'Pro 版', enterprise: '企业版' } const map = { free: '免费版', private: '私有化部署', buyout: '买断源码' }
return map[id] || id return map[id] || id
} }
const handleUpgrade = async () => { const handleSelect = () => {
if (!selected.value || selected.value === currentPlan.value) return const p = plans.value.find(x => x.id === selected.value)
loading.value = true if (!p) return
try { if (p.action === 'trial') {
// #ifdef MP-WEIXIN uni.showToast({ title: '已使用免费版', icon: 'success' })
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
return return
} }
contactType.value = p.id
if (res.pay_type === 'jsapi' && res.pay_params) { form.value = { name: '', company: '', phone: '', message: '' }
uni.requestPayment({ showContact.value = true
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
} }
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) { } catch (err) {
uni.showToast({ title: err.message || '操作失败', icon: 'none' }) uni.showToast({ title: err.message || '提交失败', icon: 'none' })
} finally { } finally {
loading.value = false contactLoading.value = false
} }
} }
</script> </script>
@@ -136,9 +130,10 @@ const handleUpgrade = async () => {
.upgrade-btn[disabled] { background: #a0cfff; } .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-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-box { background: #fff; border-radius: 20rpx; padding: 40rpx; width: 560rpx; }
.qr-title { font-size: 30rpx; font-weight: 600; color: #333; margin-bottom: 30rpx; display: block; } .qr-title { font-size: 30rpx; font-weight: 600; color: #333; margin-bottom: 24rpx; display: block; text-align: center; }
.qr-img { width: 300rpx; height: 300rpx; display: block; margin: 0 auto 30rpx; } .c-input { border: 2rpx solid #e8e8e8; border-radius: 10rpx; padding: 16rpx; font-size: 26rpx; margin-bottom: 16rpx; width: 100%; box-sizing: border-box; }
.qr-hint { font-size: 24rpx; color: #999; display: block; margin-bottom: 20rpx; } .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; }
.qr-close { font-size: 26rpx; color: #1890ff; display: block; } .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> </style>
+4
View File
@@ -259,6 +259,10 @@ export const paymentApi = {
request('/payment/create-order', 'POST', { plan, pay_type: payType }), request('/payment/create-order', 'POST', { plan, pay_type: payType }),
} }
export const leadApi = {
create: (data) => request('/leads', 'POST', data),
}
export const creditApi = { export const creditApi = {
balance: () => request('/credits/balance'), balance: () => request('/credits/balance'),
history: (page = 1, size = 20) => request(`/credits/history?page=${page}&size=${size}`), history: (page = 1, size = 20) => request(`/credits/history?page=${page}&size=${size}`),
+30 -1
View File
@@ -2,7 +2,36 @@
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="description" content="TradeMate 工作台 — 外贸小助手的用户工作台。智能翻译、客户管理、营销文案、报价单生成、WhatsApp 集成,一站式外贸全流程工具。" />
<meta name="keywords" content="外贸工作台,TradeMate,外贸小助手,AI翻译,客户管理,营销文案,报价单,WhatsApp" />
<meta name="author" content="北京宇之然科技中心" />
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#1890ff" />
<link rel="canonical" href="https://trade.yuzhiran.com/workspace/" />
<meta property="og:type" content="website" />
<meta property="og:title" content="TradeMate 工作台" />
<meta property="og:description" content="外贸小助手的用户工作台 — AI 翻译、客户管理、营销文案、报价单、WhatsApp 集成" />
<meta property="og:url" content="https://trade.yuzhiran.com/workspace/" />
<meta property="og:site_name" content="TradeMate" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="TradeMate 工作台" />
<meta name="twitter:description" content="外贸小助手的用户工作台" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "TradeMate 工作台",
"url": "https://trade.yuzhiran.com/workspace/",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web",
"description": "外贸小助手的用户工作台 — AI 翻译、客户管理、营销文案、报价单、WhatsApp 集成"
}
</script>
<title>TradeMate 工作台</title> <title>TradeMate 工作台</title>
</head> </head>
<body> <body>
+7
View File
@@ -0,0 +1,7 @@
User-agent: *
Allow: /
Disallow: /api/
Disallow: /static/
Disallow: /assets/
Sitemap: https://trade.yuzhiran.com/sitemap.xml
+2
View File
@@ -135,6 +135,8 @@ export function subscribeCreditPlan(planId, payType = 'alipay') {
} }
export function cancelCreditSubscription() { return http.post('/credits/cancel-subscription') } 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 startAgentPipeline(data) { return http.post('/agent/start', data, { timeout: 300000 }) }
export function listAgentPipelines(params) { return http.get('/agent/pipelines', { params }) } export function listAgentPipelines(params) { return http.get('/agent/pipelines', { params }) }
export function getAgentPipeline(id) { return http.get(`/agent/${id}`) } 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> <button class="modal-x" @click="close">&times;</button>
</div> </div>
<div class="modal-plans" v-loading="loading"> <div class="modal-summary">
<div <div class="summary-row"><strong>免费试用</strong><span>30 积分 + 每日 1000 字翻译零门槛体验</span></div>
v-for="p in displayPlans" <div class="summary-row"><strong>私有化部署</strong><span>独立部署数据私有不限账号年付授权</span></div>
:key="p.id" <div class="summary-row"><strong>买断源码</strong><span>完整源码可二次开发永久授权</span></div>
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>
<div class="modal-foot"> <div class="modal-foot">
<span class="hint">订阅后网页端浏览器插件Skills 通用</span> <span class="hint">网页端浏览器插件Skills 通用</span>
<el-button text size="small" @click="goCreditsPage">购买积分包低至 ¥2.9</el-button> <el-button type="primary" size="small" @click="goUpgradePage">查看完整方案</el-button>
</div> </div>
</div> </div>
</div> </div>
@@ -62,10 +26,8 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue' import { ref, watch, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { getSubscriptionPlans, subscribeCreditPlan } from '@/api'
const props = defineProps({ const props = defineProps({
visible: { type: Boolean, default: false }, visible: { type: Boolean, default: false },
@@ -76,93 +38,26 @@ const props = defineProps({
const emit = defineEmits(['update:visible']) const emit = defineEmits(['update:visible'])
const router = useRouter() 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() { function close() {
emit('update:visible', false) emit('update:visible', false)
} }
function goUpgradePage() {
close()
router.push('/workspace/upgrade')
}
// Listen for global upgrade event (from 402 interceptor or other components) // Listen for global upgrade event (from 402 interceptor or other components)
function onUpgradeEvent(e) { 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 if (router.currentRoute?.value?.path?.includes('/upgrade')) return
emit('update:visible', true) emit('update:visible', true)
} }
watch(() => props.visible, (v) => { watch(() => props.visible, () => {})
if (v) loadPlans()
})
onMounted(() => { onMounted(() => {
window.addEventListener('trademate:upgrade', onUpgradeEvent) window.addEventListener('trademate:upgrade', onUpgradeEvent)
if (props.visible) loadPlans()
}) })
onUnmounted(() => { onUnmounted(() => {
@@ -195,10 +90,15 @@ onUnmounted(() => {
background: none; border: none; font-size: 24px; color: #94a3b8; cursor: pointer; background: none; border: none; font-size: 24px; color: #94a3b8; cursor: pointer;
} }
.modal-x:hover { color: #64748b; } .modal-x:hover { color: #64748b; }
.modal-plans { .modal-summary {
padding: 20px 24px; display: flex; gap: 12px; padding: 16px 24px; display: flex; flex-direction: column; gap: 10px;
min-height: 260px;
} }
.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 { .plan-card {
flex: 1; border: 1px solid #e5e7eb; border-radius: 12px; flex: 1; border: 1px solid #e5e7eb; border-radius: 12px;
padding: 16px; text-align: center; position: relative; padding: 16px; text-align: center; position: relative;
+1
View File
@@ -22,6 +22,7 @@
@select="showMobileMenu = false" @select="showMobileMenu = false"
> >
<el-menu-item index="/workspace"><el-icon><Odometer /></el-icon><span>{{ $t('nav.home') }}</span></el-menu-item> <el-menu-item index="/workspace"><el-icon><Odometer /></el-icon><span>{{ $t('nav.home') }}</span></el-menu-item>
<el-menu-item index="/workspace/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/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/biz"><el-icon><Goods /></el-icon><span>{{ $t('nav.biz') }}</span></el-menu-item>
<el-menu-item index="/workspace/analytics"><el-icon><DataAnalysis /></el-icon><span>{{ $t('nav.analytics') }}</span></el-menu-item> <el-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: '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: '数据分析' } },
{ path: 'agent', name: 'Agent', component: () => import('@/views/Agent.vue'), meta: { title: 'AI数字员工' } },
{ path: 'team', name: 'Team', component: () => import('@/views/Team.vue'), meta: { title: '团队协作' } }, { path: 'team', name: 'Team', component: () => import('@/views/Team.vue'), meta: { title: '团队协作' } },
{ path: 'profile', name: 'Profile', component: () => import('@/views/Profile.vue'), meta: { title: '个人中心' } }, { path: 'profile', name: 'Profile', component: () => import('@/views/Profile.vue'), meta: { title: '个人中心' } },
{ path: 'profile/credits', name: 'Credits', component: () => import('@/views/Credits.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: '/invoice', redirect: '/workspace/profile/invoice' },
{ path: '/notifications', redirect: '/workspace/profile/notifications' }, { path: '/notifications', redirect: '/workspace/profile/notifications' },
{ path: '/feedback', redirect: '/workspace/profile/feedback' }, { path: '/feedback', redirect: '/workspace/profile/feedback' },
{ path: '/agent', redirect: '/workspace' }, { path: '/agent', redirect: '/workspace/agent' },
{ path: '/discovery', redirect: '/workspace/customers' }, { path: '/discovery', redirect: '/workspace/customers' },
{ path: '/followup', redirect: '/workspace/customers' }, { path: '/followup', redirect: '/workspace/customers' },
{ path: '/marketing', redirect: '/workspace/biz' }, { path: '/marketing', redirect: '/workspace/biz' },
+97 -240
View File
@@ -1,254 +1,134 @@
<template> <template>
<div class="upgrade-page"> <div class="upgrade-page">
<div class="page-head"> <div class="page-head">
<h1>选择适合你的套餐</h1> <h1>选择适合你的方案</h1>
<p class="page-sub">订阅后所有产品线通用 网页工作台浏览器插件Agent Skills</p> <p class="page-sub">TradeMate 提供免费试用私有化部署与源码买断三种方式按需选择</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>
</div> </div>
<div class="plans-grid" v-loading="loading"> <div class="plans-grid" v-loading="loading">
<!-- Free Tier --> <!-- Free Trial -->
<div class="plan-card" :class="{ current: currentPlan === 'free' }"> <div class="plan-card free">
<div class="plan-name">Free</div> <div class="plan-badge">推荐体验</div>
<div class="plan-price free">免费</div> <div class="plan-name">免费试用</div>
<div class="plan-credits">30 积分一次性</div> <div class="plan-price free">¥0</div>
<div class="plan-credits">30 积分一次性+ 每日 1000 字免费翻译</div>
<ul class="plan-features"> <ul class="plan-features">
<li>每日 1000 字免费翻译</li> <li>AI 翻译 / 智能回复</li>
<li>基本功能体验</li> <li>客户发现 / 营销生成</li>
<li>用完即止</li> <li>报价单 / 客户健康度</li>
<li>网页端浏览器插件通用</li>
</ul> </ul>
<el-button type="default" disabled class="plan-btn" v-if="currentPlan === 'free'">当前套餐</el-button> <el-button type="primary" class="plan-btn" @click="startTrial">免费使用</el-button>
<el-button type="default" disabled class="plan-btn" v-else>当前套餐</el-button>
</div> </div>
<!-- Dynamically loaded plan cards --> <!-- Private Deployment -->
<div <div class="plan-card featured">
v-for="p in planCards" <div class="plan-badge">企业首选</div>
:key="p.id" <div class="plan-name">私有化部署</div>
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"> <div class="plan-price">
¥{{ billingPeriod === 'yearly' ? p.yearlyPrice : p.price }} ¥{{ privatePlan.price }}<small>/{{ privatePlan.unit }}</small>
<small>/{{ billingPeriod === 'yearly' ? '年' : '月' }}</small>
</div> </div>
<div v-if="billingPeriod === 'yearly' && p.yearlyOriginal" class="plan-original"> <div class="plan-credits">{{ privatePlan.credits }}</div>
<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"> <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> </ul>
<el-button <el-button type="primary" class="plan-btn" @click="openContact('private')">申请私有化部署</el-button>
v-if="p.isCurrent" </div>
type="default"
disabled <!-- Source Buyout -->
class="plan-btn" <div class="plan-card">
>当前套餐</el-button> <div class="plan-badge">一次买断</div>
<el-button <div class="plan-name">买断源码</div>
v-else <div class="plan-price">
type="primary" ¥{{ buyoutPlan.price }}<small>/一次性</small>
class="plan-btn" </div>
:loading="payingId === p.id" <div class="plan-credits">{{ buyoutPlan.credits }}</div>
@click="handleUpgrade(p)" <ul class="plan-features">
>升级到 {{ p.name }}</el-button> <li>完整前端 + 后端源码</li>
<li>可自由二次开发</li>
<li>永久授权无后续费用</li>
<li>社区与文档支持</li>
</ul>
<el-button type="primary" class="plan-btn" @click="openContact('buyout')">咨询源码买断</el-button>
</div> </div>
</div> </div>
<!-- Feature Comparison Table --> <!-- Contact dialog for enterprise offerings -->
<el-card class="comparison-card" v-if="planCards.length"> <el-dialog v-model="contact.visible" :title="contactTitle" width="420px">
<template #header><strong>完整功能对比</strong></template> <el-form :model="contact.form" label-width="80px">
<el-table :data="comparisonRows" border stripe> <el-form-item label="称呼" required>
<el-table-column prop="feature" label="功能" width="160" /> <el-input v-model="contact.form.name" placeholder="您的称呼" />
<el-table-column prop="free" label="Free" width="120" align="center" /> </el-form-item>
<el-table-column v-for="p in planCards" :key="p.id" :prop="p.id" :label="p.name" width="130" align="center" /> <el-form-item label="公司">
</el-table> <el-input v-model="contact.form.company" placeholder="公司名称(选填)" />
</el-card> </el-form-item>
<el-form-item label="手机号" required>
<!-- Package section --> <el-input v-model="contact.form.phone" placeholder="用于商务联系" />
<el-card class="package-card"> </el-form-item>
<template #header><strong>积分包无需订阅按需购买</strong></template> <el-form-item label="需求">
<div class="package-grid"> <el-input v-model="contact.form.message" type="textarea" :rows="3" placeholder="部署规模 / 定制需求(选填)" />
<div v-for="pkg in packages" :key="pkg.id" class="package-item"> </el-form-item>
<div class="pkg-name">{{ pkg.name }}</div> </el-form>
<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>
<template #footer> <template #footer>
<el-button @click="payDialog.visible = false">取消</el-button> <el-button @click="contact.visible = false">取消</el-button>
<el-button type="primary" @click="confirmPay" :loading="payDialog.loading">确认支付</el-button> <el-button type="primary" :loading="contact.loading" @click="submitContact">提交申请</el-button>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { import { submitLead } from '@/api'
getSubscriptionPlans, getCreditPackages, getCreditBalance,
subscribeCreditPlan, purchaseCreditPackage,
} from '@/api'
const billingPeriod = ref('monthly') const router = useRouter()
const loading = ref(false) 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, visible: false,
type: 'subscription', // 'subscription' | 'package' type: 'private',
plan: null,
pkg: null,
payType: 'alipay',
loading: false, loading: false,
form: { name: '', company: '', phone: '', message: '' },
}) })
const contactTitle = computed(() => contact.value.type === 'private' ? '申请私有化部署' : '咨询源码买断')
const PLAN_META = { function startTrial() {
starter: { badge: '入门', yearlyDiscount: 0.17, features: ['200 积分/月', '无每日限制', '翻译 + 客户发现 + 营销生成', '智能回复 + 报价单'] }, router.push('/workspace')
pro: { badge: '推荐', featured: true, yearlyDiscount: 0.15, features: ['1000 积分/月', 'AI 数字员工', '团队协作(3 人)', '优先技术支持'] },
enterprise: { badge: '旗舰', yearlyDiscount: 0.16, features: ['2500 积分/月', '不限团队人数', 'API 调用权限', 'SLA 保障'] },
} }
function openContact(type) {
const planCards = computed(() => { contact.value.type = type
return plans.value.map(p => { contact.value.form = { name: '', company: '', phone: '', message: '' }
const meta = PLAN_META[p.id] || {} contact.value.visible = true
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 async function submitContact() {
}) const f = contact.value.form
if (!f.name.trim() || !f.phone.trim()) {
const comparisonRows = computed(() => { ElMessage.warning('请填写称呼与手机号')
const features = [ return
{ 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, '✓'])) }, contact.value.loading = true
{ 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, pkgsRes, balanceRes] = await Promise.all([ await submitLead({ type: contact.value.type, ...f })
getSubscriptionPlans().catch(() => []), ElMessage.success('提交成功,商务会尽快联系您')
getCreditPackages().catch(() => []), contact.value.visible = false
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
} catch (e) { } 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> </script>
<style scoped> <style scoped>
@@ -256,9 +136,7 @@ onMounted(loadData)
.page-head { text-align: center; margin-bottom: 32px; } .page-head { text-align: center; margin-bottom: 32px; }
.page-head h1 { font-size: 28px; color: #1e293b; margin: 0 0 8px; } .page-head h1 { font-size: 28px; color: #1e293b; margin: 0 0 8px; }
.page-sub { font-size: 14px; color: #64748b; margin: 0 0 20px; } .page-sub { font-size: 14px; color: #64748b; margin: 0 0 20px; }
.billing-toggle { display: inline-flex; align-items: center; gap: 8px; } .plans-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }
.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 { .plan-card {
background: #fff; border: 1px solid #e5e7eb; border-radius: 16px; background: #fff; border: 1px solid #e5e7eb; border-radius: 16px;
padding: 24px 20px; text-align: center; position: relative; 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: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.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 { .plan-badge {
position: absolute; top: -10px; left: 50%; transform: translateX(-50%); position: absolute; top: -10px; left: 50%; transform: translateX(-50%);
background: #2563eb; color: #fff; font-size: 11px; padding: 2px 14px; background: #2563eb; color: #fff; font-size: 11px; padding: 2px 14px;
border-radius: 10px; font-weight: 600; 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 { 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: 30px; font-weight: 800; color: #2563eb; margin: 8px 0 2px; }
.plan-price { font-size: 28px; font-weight: 800; color: #2563eb; margin: 8px 0 2px; } .plan-price.free { color: #64748b; font-size: 22px; }
.plan-price.free { color: #64748b; font-size: 20px; }
.plan-price small { font-size: 14px; font-weight: 400; color: #64748b; } .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-credits { font-size: 13px; color: #64748b; margin-bottom: 12px; }
.plan-features { list-style: none; padding: 0; margin: 0 0 16px; } .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 { font-size: 13px; color: #475569; line-height: 2; }
.plan-features li::before { content: '✓ '; color: #52c41a; font-weight: 700; } .plan-features li::before { content: '✓ '; color: #52c41a; font-weight: 700; }
.plan-btn { width: 100%; } .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>