refactor(i18n): 全工程面向国际用户与全球现代商务重构,彻底清理出海跨境等单向视角表达并对齐自动化测试
这个提交包含在:
159
AGENTS.md
159
AGENTS.md
@@ -8,10 +8,10 @@
|
||||
|
||||
- **项目名称**:OmniCard AI (全球智能数字名片 & 移动微官网 SaaS)
|
||||
- **远程仓库**:`https://gitcode.gynhc.com/wikiboss/OmniCard.git`
|
||||
- **业务定位**:面向全球商务出海的下一代 **AI 驱动型智能名片 + 移动微官网 + 访客雷达 CRM + NFC 实体卡管理** 一体化平台。
|
||||
- **业务定位**:面向全球现代商务、跨国团队与专业顾问的新一代 **AI 驱动型智能名片 + 移动微官网 + 访客雷达 CRM + NFC 实体卡管理** 一体化平台。
|
||||
- **业务核心模块**:
|
||||
1. 微官网搭建与展示系统(Bento Grid 便当盒美学、响应式与 RTL 多语言适配)
|
||||
2. 访客雷达与轻量级出海 CRM(国家定位、停留轨迹、意向度评分、线索跟进)
|
||||
2. 访客雷达与轻量级商务 CRM(国家定位、停留轨迹、意向度评分、线索跟进)
|
||||
3. AI 智能中枢与数字人顾问(全天候接待客商、智能留资、多语言地道润色)
|
||||
4. NFC 实体卡硬件云端生命周期管理(场景切换、远程挂失)
|
||||
5. 企业团队管理与多租户商业化订阅体系
|
||||
@@ -83,7 +83,7 @@ CRMmp/
|
||||
- **类型安全**:全面采用 TypeScript,严禁滥用 `any`。严格定义 Props 接口与服务端 API 响应契约。
|
||||
- **状态与样式**:
|
||||
- 遵循现代 React 最佳实践(Hooks、函数组件、避免多余状态冗余)。
|
||||
- 优先复用 Arco 官方样式变量与工具类,保持设计一致性与出海高端商务调性。
|
||||
- 优先复用 Arco 官方样式变量与工具类,保持设计一致性与国际高端商务调性。
|
||||
|
||||
### 3.3 用户端:uni-app x (App + H5) 规范 (目录: mpuniapp/)
|
||||
- **语法与语言**:
|
||||
@@ -178,100 +178,97 @@ CRMmp/
|
||||
更新版,已把**风格约束**一起并入 AI Coding 约束。可直接放入 `.cursorrules` / `CLAUDE.md`。
|
||||
|
||||
```md
|
||||
# NexaCard AI — AI Coding 约束
|
||||
# OmniCard AI — AI Coding & UI/UX 核心设计约束
|
||||
|
||||
## 风格定位
|
||||
- 瑞士国际主义 + 现代 SaaS + 编辑感
|
||||
- 内容优先,留白即品牌,AI 隐形,CRM 无感,NFC 轻量
|
||||
- 全球商务、中性、可信、克制
|
||||
- 禁止炫技、拟物、霓虹、玻璃拟态、重装饰
|
||||
- 一屏只允许 1 个主 CTA
|
||||
## 1. 风格定位 (Aesthetic & Style)
|
||||
- **Bento Grid(便当盒美学)模组化布局 + 现代消费级全球化 SaaS(Consumerized B2B SaaS)**
|
||||
- **场景优先(Context-First)**:移动端名片出示场景采用**沉浸式全屏大二维码展台** + **底部大圆角浮层抽屉(Bottom Sheet)**,物理 NFC 实体卡与数字名片具象化联动
|
||||
- **极简科技纯净感**:中性冷白底色为主,辅以科技天蓝与局部极光弥散微光晕(Ambient Aurora / Mesh Gradient)
|
||||
- **全球现代商务**:可信、现代、通透、高呼吸感与留白,杜绝冰冷传统表单后台感
|
||||
- **一屏仅允许 1 个核心主 CTA**(悬浮胶囊式或居中凸起发送快捷按钮)
|
||||
|
||||
## 技术栈
|
||||
|
||||
## 设计 Token(必须引用,禁止硬编码)
|
||||
## 2. 设计 Token (Design Tokens - 必须引用,禁止硬编码)
|
||||
颜色 Light / Dark:
|
||||
- primary: #0B5CFF / #4D8DFF
|
||||
- primary-soft: #EAF1FF / #102A4D
|
||||
- ai: #7C5CFF / #9B8CFF
|
||||
- bg: #FFFFFF / #0B0D10
|
||||
- surface: #F7F8FA / #12151A
|
||||
- border: #E3E7ED / #262B33
|
||||
- text: #101828 / #F2F4F7
|
||||
- text-2: #667085 / #98A2B3
|
||||
- primary: #0088FF / #3894FF(科技天蓝/钴蓝)
|
||||
- primary-gradient: linear-gradient(135deg, #00A3FF 0%, #0B63E5 100%)
|
||||
- primary-soft: #EBF5FF / #0D2847
|
||||
- aurora: linear-gradient(135deg, #00D2FF 0%, #0088FF 50%, #6366F1 100%)(极光弥散)
|
||||
- bg: #F5F8FC / #0B0D10(冷白天幕底色)
|
||||
- surface: #FFFFFF / #13171F(纯白卡片容器)
|
||||
- border: #EBF1F7 / #232936(极浅分割微线)
|
||||
- text: #101828 / #F2F4F7(主内容高对比)
|
||||
- text-2: #64748B / #94A3B8(次级说明文字)
|
||||
- success: #12B76A / #32D583
|
||||
- warning: #F79009 / #FDB022
|
||||
- error: #F04438 / #F97066
|
||||
|
||||
间距:4 / 8 / 12 / 16 / 24 / 32 / 48 / 64
|
||||
圆角:按钮 12px,卡片 16px,模态 20px
|
||||
阴影:0 1px 2px rgba(16,24,40,0.04)
|
||||
字体:Inter, Noto Sans SC, Noto Sans Arabic, Noto Sans JP, Noto Sans KR
|
||||
字号:12 / 14 / 16 / 20 / 24 / 32 / 40
|
||||
间距:4 / 8 / 12 / 16 / 20 / 24 / 32 / 48 / 64
|
||||
平滑大圆角 (Squircle):
|
||||
- 按钮与胶囊标签: 14px / 9999px (Pill)
|
||||
- Bento 卡片与内容容器: 20px / 24px
|
||||
- 移动端底部抽屉与模态面板: 顶角 24px / 28px
|
||||
- 二维码展台卡片: 20px
|
||||
漫反射环境软阴影 (Ambient Soft Shadow):
|
||||
- shadow-card: 0 8px 30px rgba(0, 0, 0, 0.04), 0 2px 8px rgba(11, 99, 229, 0.03)
|
||||
- shadow-cta: 0 8px 20px rgba(0, 136, 255, 0.22)
|
||||
- shadow-float: 0 12px 36px rgba(16, 24, 40, 0.08)
|
||||
字体:Inter, Noto Sans SC, Noto Sans Arabic, Noto Sans JP, Noto Sans KR, SF Pro Display
|
||||
字号:12 / 14 / 16 / 18 / 20 / 24 / 32 / 40
|
||||
行高:正文 1.6,标题 1.25,阿语 1.7
|
||||
最大宽度:内容 1200px,文章 720px
|
||||
最大宽度:内容 1200px,文章 720px,移动端自适应 480px
|
||||
行宽:45–75 字符
|
||||
|
||||
## 配色规则
|
||||
- 中性色占 80–90%,品牌色占 10–20%
|
||||
- 品牌主色只用单一钴蓝
|
||||
- 语义色只用于状态,不做装饰
|
||||
- 禁止红、绿、黄作为品牌主色
|
||||
- AI 渐变只允许小面积,面积 <5%
|
||||
- NFC 用主色 20% 透明脉冲,一次 400ms,不循环
|
||||
## 3. 配色与光影规则
|
||||
- **比例原则**:纯净冷白与中性色占 85%–90%,品牌科技蓝占 10%–15%
|
||||
- **受控弥散光晕**:极光渐变(Aurora)仅限局部高价值区域(沉浸式展示头部、品牌卡、NFC 实体卡演示位、雷达高光),面积 < 8%,禁止全屏杂乱渐变
|
||||
- **环境软阴影**:以超大半径、极淡漫反射阴影营造卡片微悬浮感,替代厚重硬边框
|
||||
- **语义色只用于状态**:成功绿、警告橙、错误红仅用于雷达意向度、系统状态与验证提示,不做大面积装饰
|
||||
- **NFC 反馈**:触碰感应触发主色 20% 透明单次微脉冲动效,时长 400ms,不循环
|
||||
|
||||
## 组件规则
|
||||
- 按钮高 40 / 44 / 48,触控区 ≥44×44
|
||||
- 输入框高 44,标签在上,错误在下
|
||||
- 卡片 1px 边框,圆角 16,无重阴影
|
||||
- 卡片最多两层嵌套,优先用分隔线
|
||||
- 图标线性 1.5px,20 / 24px
|
||||
- 焦点环 2px primary,offset 2,必须可见
|
||||
- 加载优先骨架屏
|
||||
- 空状态:一句话 + 一个主 CTA
|
||||
- 悬停仅 2–4% 明暗变化,不放大
|
||||
## 4. 核心组件与交互规范
|
||||
- **Bento Grid 模组规范**:各卡片高度内聚自洽(趋势折线图、访客雷达通知条、个人 Profile、NFC 硬件状态、隐私时效控制),网格弹性响应
|
||||
- **移动端沉浸展台模式 (Showcase Mode)**:
|
||||
- 顶部占屏约 45% 的科技蓝/极光底色,大号高清晰二维码居中直出,配 `Scan to share card` 微光引导
|
||||
- 底部大圆角抽屉 (Bottom Sheet) 承载:一键复制直链栏(直链 + `COPY` 胶囊)、绿色可持续发展理念文案、实体白卡拟物微轮播
|
||||
- 底部悬浮 TabBar:Contacts、Share(主 CTA 胶囊凸起微阴影)、Privacy
|
||||
- **访客雷达实时动态通知条**:
|
||||
- 采用轻微圆角悬浮条(如 "Alex saved your contact card Just Now"),左侧品牌 Logo/头像,白底轻投影
|
||||
- **隐私时效控制控件**:
|
||||
- 结构化时效行(Temporary Visibility / Extended Visibility)搭配平滑展开行与单选/滑块
|
||||
- **按钮与触控区**:高度 40 / 44 / 48,触控热区 ≥ 44×44,悬停仅 2–4% 明暗微变化,不放大
|
||||
- **图标体系**:统一 1.5px 极细线性图标,规格 20 / 24px
|
||||
|
||||
## 动效
|
||||
- 微交互 150ms,抽屉 250ms,页面 400ms
|
||||
## 5. 动效规范
|
||||
- 微交互 150ms,抽屉滑出/收起 250ms,页面转场 350ms
|
||||
- 缓动:cubic-bezier(0.2, 0.8, 0.2, 1)
|
||||
- 支持 prefers-reduced-motion
|
||||
- AI 用微光,不用打字机作为唯一反馈
|
||||
- NFC 用一次脉冲,不循环
|
||||
- 极光微光晕呼吸缓慢流动,支持 prefers-reduced-motion
|
||||
- AI 采用微光晕浮现,不以打字机作为单一阻塞反馈
|
||||
|
||||
## 代码规范
|
||||
- 严格 TypeScript,禁止 any
|
||||
- 组件 PascalCase,函数式
|
||||
- 样式只用 Tailwind,禁止内联 style
|
||||
- 所有文案走 i18n key,禁止硬编码
|
||||
- 颜色 / 间距 / 字体必须用 token,禁止魔法值
|
||||
- 支持暗色模式,用 CSS 变量
|
||||
- 支持 RTL,用 logical properties(ms / me / ps / pe)
|
||||
- 语义化标签 + aria + 键盘可达
|
||||
- 动效必须支持 reduced-motion
|
||||
## 6. 代码与工程规范
|
||||
- 严格 TypeScript / uts,严禁使用 any
|
||||
- 组件 PascalCase,函数式,统一使用设计 Token,严禁内联魔法值
|
||||
- 所有文案必须走 i18n key,禁止硬编码文字
|
||||
- 支持暗色模式,采用 CSS/UTS 主题变量
|
||||
- 支持 RTL,一律采用逻辑属性(ms / me / ps / pe)
|
||||
- 语义化标签 + 键盘可达 + 无障碍 aria 规范
|
||||
|
||||
## 国际化
|
||||
- next-intl,默认 en
|
||||
- 独立 URL:/en /es /fr
|
||||
- hreflang + sitemap
|
||||
- 日期 / 货币 / 时区本地化
|
||||
- 按钮和标签预留 30% 文本扩展
|
||||
- 社交适配:WhatsApp、LinkedIn、Telegram、Line、KakaoTalk、微信
|
||||
- 支付:Stripe、PayPal
|
||||
- GDPR、CCPA、Cookie 同意、数据导出 / 删除
|
||||
- WCAG 2.2 AA,正文对比度 ≥ 4.5:1
|
||||
- 不强制登录看名片,不自动播放声音
|
||||
## 7. 国际化与全球合规标准
|
||||
- 国际多语言优先(默认 en,支持多语言独立路由或上下文切换)
|
||||
- 国际主流社交一键触达:WhatsApp、LinkedIn、Telegram、Line、KakaoTalk、WeChat
|
||||
- 支付与协议:Stripe、PayPal,名片遵循 vCard (RFC 6350)
|
||||
- 合规:GDPR、CCPA、Cookie 同意、隐私时效自毁
|
||||
- WCAG 2.2 AA 标准,文本对比度 ≥ 4.5:1
|
||||
|
||||
## 禁止
|
||||
- 纯黑 #000
|
||||
- 超过 3 种字体
|
||||
- 大面积渐变 / 重阴影 / 霓虹
|
||||
- 硬编码文案或颜色
|
||||
- 固定宽度按钮
|
||||
- 强制登录看名片
|
||||
- 自动播放声音
|
||||
- 一屏多个主按钮
|
||||
- 装饰性插画 / 拟物图标 / 文化特定符号
|
||||
- 禁止使用emoji
|
||||
## 8. 禁止事项 (Strictly Prohibited)
|
||||
- 严禁使用纯黑 #000
|
||||
- 严禁生硬、灰暗、传统冷冰冰的表单式名片页(禁止将二维码埋入杂乱表单角中)
|
||||
- 严禁大面积土味高饱和霓虹渐变、重阴影与过度玻璃拟态
|
||||
- 严禁超过 3 种字体
|
||||
- 严禁硬编码文案、固定宽度按钮
|
||||
- 严禁强制登录才可浏览名片,严禁自动播放声音
|
||||
- 严禁一屏出现多个主按钮
|
||||
- 严禁装饰性插画、拟物大图标与特定文化敏感符号
|
||||
- 严禁在正式生产界面中使用非业务硬编码 emoji
|
||||
```
|
||||
|
||||
### 翻译要求
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# OmniCard AI (全球智能数字名片 & 移动微官网 SaaS)
|
||||
|
||||
> 面向全球商务出海的下一代 **AI 驱动型智能名片 + 移动微官网 + 访客雷达 CRM + NFC 实体卡管理** 一体化平台。
|
||||
> 面向全球现代商务、跨国团队与专业顾问的下一代 **AI 驱动型智能名片 + 移动微官网 + 访客雷达 CRM + NFC 实体卡管理** 一体化平台。
|
||||
|
||||
---
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
- 🎨 **[体验设计大师规范 (UI/UX Design Spec)](./docs/UI_UX_DESIGN_SPEC.md)**
|
||||
- “Touch & Spark” 零阻力触达设计哲学
|
||||
- Bento Grid (便当盒网格) 响应式美学
|
||||
- 全球化出海色彩系统与字体规范(包含阿语 RTL 镜像布局)
|
||||
- 全球化设计色彩系统与字体规范(包含阿语 RTL 镜像布局)
|
||||
- 关键页面组件字典(访客端主页、AI 顾问浮层、NFC 交互触感、CRM 销售看板)
|
||||
- 性能与首屏秒开体验预算 (FCP < 500ms)
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
## 🌟 核心价值亮点
|
||||
|
||||
1. **零摩擦触达**:访客无需注册、无需下载 App,NFC 触碰或扫码即可瞬间打开,一键导入手机通讯录 (`.vcf`)。
|
||||
2. **多语言与智能出海**:支持英、西、阿 (RTL)、中、德、日等国际主流语种,AI 不仅支持翻译更提供地道商务润色。
|
||||
3. **AI 24/7 数字化身**:名片内置基于企业/个人知识库的 AI 问答代表,全天候接待跨时区海外客商并智能留资。
|
||||
2. **多语言与智能本地化**:支持英、西、阿 (RTL)、中、德、日等国际主流语种,AI 不仅支持翻译更提供地道商务润色。
|
||||
3. **AI 24/7 数字化身**:名片内置基于企业/个人知识库的 AI 问答代表,全天候接待跨时区全球客户并智能留资。
|
||||
4. **访客雷达与轻量 CRM**:实时掌握客户访问动态(国家定位、停留时长、阅读案例偏好),智能生成意向度评分与跟进话术。
|
||||
5. **NFC 动态云端硬件管理**:实体卡一次购入,云端自由切换业务场景(商务展会模式 / 社交模式 / 纯通讯录模式),支持一键远程挂失。
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ const i18n = {
|
||||
'menu.card': '数字名片',
|
||||
'menu.card.list': '名片资产库',
|
||||
'menu.card.builder': 'Bento 搭建器',
|
||||
'menu.card.cases': '出海案例作品集',
|
||||
'menu.card.cases': '案例作品集',
|
||||
'menu.card.articles': '文章与动态',
|
||||
'menu.crm': '访客雷达',
|
||||
'menu.crm.leads': '潜客线索',
|
||||
|
||||
@@ -66,9 +66,9 @@ interface PersonaPresetDetail {
|
||||
const PRESET_DATA: Record<ConciergePersonaType, PersonaPresetDetail> = {
|
||||
trade_expert: {
|
||||
welcomeMsg:
|
||||
'您好!我是您的专属出海业务顾问。请问有什么全球贸易或合作需求可以协助您?',
|
||||
'您好!我是您的专属商务顾问。请问有什么业务咨询或国际合作需求可以协助您?',
|
||||
promptTemplate:
|
||||
'你是一名深耕国际贸易与跨国商务的出海业务专家。用专业、沉稳的商务语调为访客解答产品出海、跨境结算、供应链合作等咨询。引导有明确意向的客商提供联系方式。',
|
||||
'你是一名深耕全球商务与跨国合作的业务顾问。用专业、沉稳的商务语调为访客解答业务合作、跨国结算、供应链方案等咨询。引导有明确意向的客商提供联系方式。',
|
||||
toneStyle: 'professional',
|
||||
triggerLeadThreshold: 70,
|
||||
},
|
||||
|
||||
@@ -77,9 +77,9 @@ const i18n = {
|
||||
'aiConcierge.form.cardSelect': '关联名片',
|
||||
'aiConcierge.form.cardSelect.placeholder': '选择或输入名片 ID',
|
||||
'aiConcierge.form.persona': '预设人设',
|
||||
'aiConcierge.persona.trade_expert': '外贸专家',
|
||||
'aiConcierge.persona.trade_expert': '国际贸易顾问',
|
||||
'aiConcierge.persona.trade_expert.desc':
|
||||
'专精跨境贸易、供应链对接与外贸商务咨询,语调稳重专业。',
|
||||
'专精全球贸易、供应链对接与国际商务咨询,语调稳重专业。',
|
||||
'aiConcierge.persona.business_rep': '商务代表',
|
||||
'aiConcierge.persona.business_rep.desc':
|
||||
'主动介绍企业竞争优势与落地案例,敏锐捕捉客商商机。',
|
||||
@@ -115,7 +115,7 @@ const i18n = {
|
||||
'aiConcierge.request.saveFail': '保存配置失败',
|
||||
'aiDeepSeek.card.title': 'DeepSeek 官方大模型底座配置',
|
||||
'aiDeepSeek.card.desc':
|
||||
'配置 DeepSeek API Key,支持 deepseek-chat 与 deepseek-reasoner 双模型接入,赋能 24/7 AI 顾问对话与出海多语言润色。',
|
||||
'配置 DeepSeek API Key,支持 deepseek-chat 与 deepseek-reasoner 双模型接入,赋能 24/7 AI 顾问对话与多语言智能润色。',
|
||||
'aiDeepSeek.form.apiKey': 'API Key 密钥',
|
||||
'aiDeepSeek.form.apiKey.placeholder':
|
||||
'输入 sk- 开头的官方密钥 (留空则保留当前已配置密钥)',
|
||||
|
||||
@@ -53,9 +53,9 @@ const i18n = {
|
||||
},
|
||||
'zh-CN': {
|
||||
'menu.ai.translation': '润色与术语库',
|
||||
'aiTranslation.playground.title': '出海文案实时润色试炼器',
|
||||
'aiTranslation.playground.title': '商务文案实时润色试炼器',
|
||||
'aiTranslation.playground.text': '待润色文本',
|
||||
'aiTranslation.playground.text.placeholder': '输入待润色的出海商务文案,系统将自动应用语气模板并强制对齐行业术语...',
|
||||
'aiTranslation.playground.text.placeholder': '输入待润色的商务文案,系统将自动应用语气模板并强制对齐行业术语...',
|
||||
'aiTranslation.playground.targetLocale': '目标语言',
|
||||
'aiTranslation.playground.targetLocale.placeholder': '请选择目标语言',
|
||||
'aiTranslation.playground.tone': '语气基调',
|
||||
|
||||
@@ -520,7 +520,7 @@ export default function CardArticlesPage() {
|
||||
field="title"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input placeholder="输入出海深度文章标题..." maxLength={120} showWordLimit />
|
||||
<Input placeholder="输入深度文章标题..." maxLength={120} showWordLimit />
|
||||
</Form.Item>
|
||||
<Form.Item label={t['cardArticles.field.summary']} field="summary">
|
||||
<Input.TextArea
|
||||
@@ -593,7 +593,7 @@ export default function CardArticlesPage() {
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
|
||||
{/* 模拟出海社交平台卡片 (LinkedIn / WhatsApp / X Card) */}
|
||||
{/* 模拟国际社交平台卡片 (LinkedIn / WhatsApp / X Card) */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: lightColors.bg,
|
||||
|
||||
@@ -43,7 +43,7 @@ const i18n = {
|
||||
'zh-CN': {
|
||||
'menu.card.articles': '文章与动态',
|
||||
'cardArticles.title': '文章与动态发布中心',
|
||||
'cardArticles.description': '为名片微官网发布深度博客与动态资讯,具备 OpenGraph 社交卡片自动提炼与出海 SEO 优化。',
|
||||
'cardArticles.description': '为名片微官网发布深度博客与动态资讯,具备 OpenGraph 社交卡片自动提炼与全球搜索引擎 SEO 优化。',
|
||||
'cardArticles.action.add': '发布新文章',
|
||||
'cardArticles.action.edit': '编辑',
|
||||
'cardArticles.action.delete': '删除',
|
||||
|
||||
@@ -70,7 +70,7 @@ interface CaseEditFormData {
|
||||
const PRESET_METRIC_TAGS: string[] = [
|
||||
'+320% ROI',
|
||||
'+150% 留资率',
|
||||
'45k+ 海外线索',
|
||||
'45k+ 国际线索',
|
||||
'2M+ 全球曝光',
|
||||
'Top 1 细分类目',
|
||||
];
|
||||
|
||||
@@ -38,10 +38,10 @@ const i18n = {
|
||||
'cases.viewPdf': 'View PDF',
|
||||
},
|
||||
'zh-CN': {
|
||||
'cases.pageTitle': '出海案例作品集',
|
||||
'cases.pageSubTitle': '面向海外客商展示出海标杆案例、量化成果指标与交付白皮书。',
|
||||
'cases.pageTitle': '标杆案例作品集',
|
||||
'cases.pageSubTitle': '面向全球合作伙伴与客商展示标杆案例、量化成果指标与交付白皮书。',
|
||||
'cases.title': '案例标题',
|
||||
'cases.titlePlaceholder': '如: 智能滑板车北美 DTC 独立站出海',
|
||||
'cases.titlePlaceholder': '如: 智能出行品牌全球 DTC 独立站建设',
|
||||
'cases.client': '客户品牌',
|
||||
'cases.clientPlaceholder': '如: VoltRide Global',
|
||||
'cases.duration': '交付周期',
|
||||
@@ -56,11 +56,11 @@ const i18n = {
|
||||
'cases.cardIdPlaceholder': '按名片 ID 筛选',
|
||||
'cases.sortOrder': '排序序号',
|
||||
'cases.description': '案例背景与简述',
|
||||
'cases.descriptionPlaceholder': '请输入项目背景、出海痛点与交付成果...',
|
||||
'cases.descriptionPlaceholder': '请输入项目背景、核心挑战与交付成果...',
|
||||
'cases.add': '新增案例',
|
||||
'cases.edit': '编辑案例',
|
||||
'cases.delete': '删除',
|
||||
'cases.deleteConfirm': '确定要删除该出海案例作品吗?',
|
||||
'cases.deleteConfirm': '确定要删除该案例作品吗?',
|
||||
'cases.search': '查询',
|
||||
'cases.reset': '重置',
|
||||
'cases.actions': '操作',
|
||||
@@ -69,8 +69,8 @@ const i18n = {
|
||||
'cases.uploadText': '点击或拖拽文件上传',
|
||||
'cases.uploadHint': '支持 JPG、PNG、WEBP 或 PDF 格式,单文件不超过 20MB',
|
||||
'cases.tagAdd': '添加 KPI 标签',
|
||||
'cases.emptyTitle': '暂无出海案例',
|
||||
'cases.emptyDesc': '创建首个标杆出海作品集,提升海外客商信任与转化效率。',
|
||||
'cases.emptyTitle': '暂无案例作品',
|
||||
'cases.emptyDesc': '创建首个标杆作品集,提升全球客商信任与转化效率。',
|
||||
'cases.operationSuccess': '操作已成功保存。',
|
||||
'cases.deleteSuccess': '案例已成功删除。',
|
||||
'cases.viewPdf': '查看 PDF',
|
||||
|
||||
@@ -531,7 +531,7 @@ export default function DashboardFinancialPage() {
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
|
||||
{/* COO 模块 B: 全球出海目标市场营收分布 */}
|
||||
{/* COO 模块 B: 全球目标市场营收分布 */}
|
||||
<Grid.Col xs={24} lg={12}>
|
||||
<Card
|
||||
title={t['dashboardFinancial.section.regionalRevenue']}
|
||||
|
||||
@@ -53,7 +53,7 @@ const i18n = {
|
||||
'dashboardFinancial.export.success': 'Reconciliation ledger exported successfully',
|
||||
},
|
||||
'zh-CN': {
|
||||
'dashboardFinancial.title': '新账与出海财务运营驾驶舱',
|
||||
'dashboardFinancial.title': '全球财务与商业化运营驾驶舱',
|
||||
'dashboardFinancial.subtitle': '全球 SaaS 商业化营收、新账拓客扩张与财务对账核销',
|
||||
'dashboardFinancial.view.coo': '运营官视角 (业务增长与扩容)',
|
||||
'dashboardFinancial.view.cfo': '财务官视角 (现金流与财税风控)',
|
||||
@@ -73,7 +73,7 @@ const i18n = {
|
||||
'dashboardFinancial.kpi.pendingReconciliation': '在途待对账款',
|
||||
'dashboardFinancial.kpi.overdueAmount': '账期逾期款项',
|
||||
'dashboardFinancial.kpi.refundAmount': '退款与争议款',
|
||||
'dashboardFinancial.kpi.estimatedTax': '跨境 VAT 预估税额',
|
||||
'dashboardFinancial.kpi.estimatedTax': '国际 VAT 预估税额',
|
||||
// COO 指标
|
||||
'dashboardFinancial.kpi.mrr': '月度经常性收入 (MRR)',
|
||||
'dashboardFinancial.kpi.arr': '年度经常性收入 (ARR)',
|
||||
@@ -83,7 +83,7 @@ const i18n = {
|
||||
'dashboardFinancial.kpi.churnRate': '客户流失率',
|
||||
// 面板标题
|
||||
'dashboardFinancial.section.planDistribution': 'SaaS 订阅套餐梯队分布',
|
||||
'dashboardFinancial.section.regionalRevenue': '全球出海目标市场营收分布',
|
||||
'dashboardFinancial.section.regionalRevenue': '全球目标市场营收分布',
|
||||
'dashboardFinancial.section.currencyBreakdown': '国际多币种汇总结算结构',
|
||||
'dashboardFinancial.section.agingAnalysis': '应收账款账龄与坏账风险模型',
|
||||
'dashboardFinancial.section.recentLedger': '最新新账与开票明细台账',
|
||||
|
||||
@@ -45,7 +45,7 @@ const i18n = {
|
||||
},
|
||||
'zh-CN': {
|
||||
'radarMap.title': '全球访客雷达热力大屏',
|
||||
'radarMap.subtitle': '出海商机实时地理轨迹、交互时长深度与多渠道触碰转化大屏。',
|
||||
'radarMap.subtitle': '全球商机实时地理轨迹、交互时长深度与多渠道触碰转化大屏。',
|
||||
'radarMap.refresh': '刷新数据',
|
||||
'radarMap.filter.allCards': '全部名片',
|
||||
'radarMap.filter.cardIdPlaceholder': '筛选名片 ID',
|
||||
|
||||
@@ -84,20 +84,87 @@ function formatDelta(value: number): string {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function formatStaySeconds(ms: number): string {
|
||||
return String(Math.max(0, Math.round(ms / 1000)));
|
||||
function formatRelativeTime(
|
||||
createTime: Date | string | undefined,
|
||||
t: Record<string, string>
|
||||
): string {
|
||||
if (!createTime) return t['workplace.radar.justNow'] || 'Just now';
|
||||
const diffMs = Date.now() - new Date(createTime).getTime();
|
||||
if (Number.isNaN(diffMs) || diffMs < 60000) {
|
||||
return t['workplace.radar.justNow'] || 'Just now';
|
||||
}
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
if (diffMins < 60) {
|
||||
return (t['workplace.radar.minsAgo'] || '{mins}m ago').replace(
|
||||
'{mins}',
|
||||
String(diffMins)
|
||||
);
|
||||
}
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
if (diffHours < 24) {
|
||||
return (t['workplace.radar.hoursAgo'] || '{hours}h ago').replace(
|
||||
'{hours}',
|
||||
String(diffHours)
|
||||
);
|
||||
}
|
||||
const date = new Date(createTime);
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${month}-${day} ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
function formatLocation(
|
||||
item: WorkplaceRadarItem,
|
||||
t: Record<string, string>
|
||||
): string {
|
||||
if (item.isLocal || item.country === 'Local' || item.city === 'Local') {
|
||||
return t['workplace.radar.localEnv'] || 'Local / Intranet';
|
||||
}
|
||||
const parts = [item.city, item.country].filter(
|
||||
(p) => Boolean(p) && p !== 'Unknown'
|
||||
);
|
||||
if (parts.length === 0) {
|
||||
return item.country || '—';
|
||||
}
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
function formatStayText(
|
||||
stayDurationMs: number,
|
||||
t: Record<string, string>
|
||||
): string {
|
||||
const sec = Math.max(0, Math.round(stayDurationMs / 1000));
|
||||
if (sec <= 3) {
|
||||
return t['workplace.radar.bounce'] || 'Quick bounce';
|
||||
}
|
||||
return (t['workplace.radar.stay'] || '{seconds}s').replace(
|
||||
'{seconds}',
|
||||
String(sec)
|
||||
);
|
||||
}
|
||||
|
||||
function computeIntentScore(item: WorkplaceRadarItem): number {
|
||||
if (typeof item.intentScore === 'number' && item.intentScore > 0) {
|
||||
return item.intentScore;
|
||||
}
|
||||
const staySec = Math.round(item.stayDurationMs / 1000);
|
||||
const blocksCount = item.viewedBlocks?.length || 0;
|
||||
let score = 40;
|
||||
if (staySec > 30) score += 20;
|
||||
if (staySec > 90) score += 20;
|
||||
score += Math.min(blocksCount * 10, 20);
|
||||
let score = 10;
|
||||
if (item.touchType === 'nfc') score = 35;
|
||||
else if (item.touchType === 'qr') score = 25;
|
||||
if (staySec > 3) score += Math.min(40, Math.round(staySec / 3));
|
||||
score += Math.min(blocksCount * 5, 20);
|
||||
return Math.min(score, 99);
|
||||
}
|
||||
|
||||
function getIntentTagColor(score: number): string {
|
||||
if (score >= 75) return 'green';
|
||||
if (score >= 45) return 'arcoblue';
|
||||
return 'gray';
|
||||
}
|
||||
|
||||
function escapeCsvCell(value: string): string {
|
||||
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
||||
return `"${value.replace(/"/g, '""')}"`;
|
||||
@@ -424,7 +491,7 @@ function Workplace(): React.ReactElement {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 出海转化漏斗 */}
|
||||
{/* 业务转化漏斗 */}
|
||||
<Card
|
||||
bordered={false}
|
||||
className={`${styles.panel} ${styles.funnelCard}`}
|
||||
@@ -530,29 +597,45 @@ function Workplace(): React.ReactElement {
|
||||
const touchType = isTouchType(item.touchType)
|
||||
? item.touchType
|
||||
: 'link';
|
||||
const locationLabel = [item.city, item.country]
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
const locationLabel = formatLocation(item, t);
|
||||
const blocks = (item.viewedBlocks || []).join(' / ');
|
||||
const intentScore = computeIntentScore(item);
|
||||
const cardLabel =
|
||||
item.cardTitle ||
|
||||
(item.cardId
|
||||
? t['workplace.radar.cardFallback'].replace('{id}', String(item.cardId))
|
||||
: '');
|
||||
const timeLabel = formatRelativeTime(item.createTime, t);
|
||||
|
||||
return (
|
||||
<Timeline.Item key={String(item.id)}>
|
||||
<div className={styles.feedMeta}>
|
||||
<Typography.Text bold>{locationLabel || '—'}</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
style={{ fontSize: 12, marginRight: 2 }}
|
||||
>
|
||||
{timeLabel}
|
||||
</Typography.Text>
|
||||
{cardLabel ? (
|
||||
<Tag color="arcoblue" size="small" style={{ borderRadius: 9999 }}>
|
||||
{cardLabel}
|
||||
</Tag>
|
||||
) : null}
|
||||
<Typography.Text bold>{locationLabel}</Typography.Text>
|
||||
<Tag color={getChannelColor(touchType)} size="small">
|
||||
{getChannelLabel(touchType)}
|
||||
</Tag>
|
||||
<Tag
|
||||
color={intentScore >= 70 ? 'green' : 'gray'}
|
||||
color={getIntentTagColor(intentScore)}
|
||||
size="small"
|
||||
>
|
||||
{t['workplace.radar.intentScore'].replace('{score}', String(intentScore))}
|
||||
{t['workplace.radar.intentScore'].replace(
|
||||
'{score}',
|
||||
String(intentScore)
|
||||
)}
|
||||
</Tag>
|
||||
<Typography.Text type="secondary" className={styles.stayTime}>
|
||||
{t['workplace.radar.stay'].replace(
|
||||
'{seconds}',
|
||||
formatStaySeconds(item.stayDurationMs)
|
||||
)}
|
||||
{formatStayText(item.stayDurationMs, t)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
{blocks ? (
|
||||
@@ -568,7 +651,7 @@ function Workplace(): React.ReactElement {
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 右侧出海运营快捷中枢 (7/24 栅格,完美填补用户红框空白) */}
|
||||
{/* 右侧全球运营快捷中枢 (7/24 栅格,完美填补用户红框空白) */}
|
||||
<Col xs={24} xl={7}>
|
||||
<div className={styles.hubContainer}>
|
||||
{/* 模块 1:触达渠道健康度 */}
|
||||
@@ -773,7 +856,7 @@ function Workplace(): React.ReactElement {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 模块 4:出海待办预警 */}
|
||||
{/* 模块 4:业务待办预警 */}
|
||||
<Card
|
||||
bordered={false}
|
||||
className={`${styles.panel} ${styles.alertPanel}`}
|
||||
|
||||
@@ -20,6 +20,12 @@ const i18n = {
|
||||
'workplace.radar.emptyAction': 'Open global radar',
|
||||
'workplace.radar.stay': '{seconds}s stay',
|
||||
'workplace.radar.viewing': 'Viewing {blocks}',
|
||||
'workplace.radar.localEnv': 'Local / Intranet Test',
|
||||
'workplace.radar.justNow': 'Just now',
|
||||
'workplace.radar.minsAgo': '{mins}m ago',
|
||||
'workplace.radar.hoursAgo': '{hours}h ago',
|
||||
'workplace.radar.bounce': 'Quick bounce',
|
||||
'workplace.radar.cardFallback': 'Card #{id}',
|
||||
'workplace.funnel.title': 'Conversion Funnel',
|
||||
'workplace.funnel.tap': 'Physical NFC',
|
||||
'workplace.funnel.view': 'Page Views',
|
||||
@@ -85,20 +91,26 @@ const i18n = {
|
||||
'workplace.radar.emptyAction': '查看全球雷达',
|
||||
'workplace.radar.stay': '停留 {seconds} 秒',
|
||||
'workplace.radar.viewing': '正在查看 {blocks}',
|
||||
'workplace.funnel.title': '出海转化漏斗',
|
||||
'workplace.radar.localEnv': '本地/局域网测试',
|
||||
'workplace.radar.justNow': '刚刚',
|
||||
'workplace.radar.minsAgo': '{mins} 分钟前',
|
||||
'workplace.radar.hoursAgo': '{hours} 小时前',
|
||||
'workplace.radar.bounce': '快速跳出',
|
||||
'workplace.radar.cardFallback': '名片 #{id}',
|
||||
'workplace.funnel.title': '业务转化漏斗',
|
||||
'workplace.funnel.tap': 'NFC 触碰',
|
||||
'workplace.funnel.view': '官网浏览',
|
||||
'workplace.funnel.interact': '意向交互',
|
||||
'workplace.funnel.lead': '商机留资',
|
||||
'workplace.channel.title': '触达渠道分布',
|
||||
'workplace.quickHub.title': '出海运营快捷中枢',
|
||||
'workplace.quickHub.title': '全球运营快捷中枢',
|
||||
'workplace.quickNav.title': '高频业务导航',
|
||||
'workplace.quick.builder': '搭建微官网',
|
||||
'workplace.quick.builderHint': '编辑便当盒微官网组件',
|
||||
'workplace.quick.leads': '潜客线索',
|
||||
'workplace.quick.leadsHint': '跟进出海海外意向买家',
|
||||
'workplace.quick.leadsHint': '跟进高意向客户与买家',
|
||||
'workplace.quick.pipeline': '商机看板',
|
||||
'workplace.quick.pipelineHint': '推进出海商机全流程赢单',
|
||||
'workplace.quick.pipelineHint': '推进全球商机全流程赢单',
|
||||
'workplace.quick.batch': '批量制卡',
|
||||
'workplace.quick.batchHint': '生成短链与批量印制物料',
|
||||
'workplace.quick.devices': '硬件台账',
|
||||
@@ -108,7 +120,7 @@ const i18n = {
|
||||
'workplace.quick.aiConcierge': 'AI 顾问',
|
||||
'workplace.quick.aiConciergeHint': '多语言接待策略与留资阈值',
|
||||
'workplace.quick.finance': '财务订阅',
|
||||
'workplace.quick.financeHint': '订阅套餐与出海营收看板',
|
||||
'workplace.quick.financeHint': '订阅套餐与全球营收看板',
|
||||
'workplace.shortcut.title': '运营效率工具箱',
|
||||
'workplace.shortcut.issueCard': '批量发卡',
|
||||
'workplace.shortcut.issueCardHint': '生成短链与制卡打印物料',
|
||||
@@ -118,7 +130,7 @@ const i18n = {
|
||||
'workplace.shortcut.radarMapHint': '查看全球访客地理分布与实时热力图',
|
||||
'workplace.shortcut.aiConfig': 'AI 顾问接待策略',
|
||||
'workplace.shortcut.aiConfigHint': '配置多语言接待话术与自动留资意向阈值',
|
||||
'workplace.alerts.title': '出海待办预警',
|
||||
'workplace.alerts.title': '业务待办预警',
|
||||
'workplace.alerts.highIntentLead': '迪拜客商在精选案例停留超过 210 秒,尚未安排专属跟进。',
|
||||
'workplace.alerts.actionFollow': '立即跟进',
|
||||
'workplace.channel.nfc': 'NFC',
|
||||
@@ -150,6 +162,12 @@ const i18n = {
|
||||
'workplace.radar.emptyAction': 'فتح الرادار العالمي',
|
||||
'workplace.radar.stay': 'مكث {seconds} ث',
|
||||
'workplace.radar.viewing': 'يشاهد {blocks}',
|
||||
'workplace.radar.localEnv': 'بيئة اختبار محلية',
|
||||
'workplace.radar.justNow': 'الآن',
|
||||
'workplace.radar.minsAgo': 'منذ {mins} دقيقة',
|
||||
'workplace.radar.hoursAgo': 'منذ {hours} ساعة',
|
||||
'workplace.radar.bounce': 'ارتداد سريع',
|
||||
'workplace.radar.cardFallback': 'بطاقة #{id}',
|
||||
'workplace.funnel.title': 'قمع التحويل',
|
||||
'workplace.funnel.tap': 'لمس NFC',
|
||||
'workplace.funnel.view': 'عرض الصفحة',
|
||||
|
||||
@@ -292,7 +292,7 @@
|
||||
}
|
||||
|
||||
/* ========================================================
|
||||
右侧出海运营快捷中枢 (Command Hub)
|
||||
右侧全球运营快捷中枢 (Command Hub)
|
||||
======================================================== */
|
||||
.hubContainer {
|
||||
display: flex;
|
||||
|
||||
@@ -38,7 +38,7 @@ const i18n = {
|
||||
},
|
||||
'zh-CN': {
|
||||
'login.form.title': '登录 OmniCard',
|
||||
'login.form.subTitle': '全球智能数字名片与出海访客雷达中枢',
|
||||
'login.form.subTitle': '全球智能数字名片与访客雷达中枢',
|
||||
'login.form.userName.errMsg': '用户名不能为空',
|
||||
'login.form.password.errMsg': '密码不能为空',
|
||||
'login.form.login.errMsg': '登录出错,请刷新重试',
|
||||
@@ -52,10 +52,10 @@ const i18n = {
|
||||
'login.form.forgetPassword': '忘记密码',
|
||||
'login.form.login': '登录',
|
||||
'login.form.register': '注册账号',
|
||||
'login.banner.badge': '全球出海企业级数字身份',
|
||||
'login.banner.badge': '全球企业级数字身份',
|
||||
'login.banner.mainTitle': '下一代全球商务智能数字名片',
|
||||
'login.banner.mainSubTitle':
|
||||
'集 NFC 实体卡管理、Bento Grid 微官网与实时访客雷达于一体的出海智能平台。',
|
||||
'集 NFC 实体卡管理、Bento Grid 微官网与实时访客雷达于一体的全球智能平台。',
|
||||
'login.banner.card1.title': '全球覆盖',
|
||||
'login.banner.card1.metric': '120+',
|
||||
'login.banner.card1.desc': '国家与地区,边缘节点毫秒分发',
|
||||
|
||||
@@ -312,7 +312,7 @@ export default function SubscribersPage() {
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="付费出海企业 (Pro/Ent)"
|
||||
title="付费企业客户 (Pro/Ent)"
|
||||
value={(overview?.proCount || 0) + (overview?.enterpriseCount || 0)}
|
||||
style={{ color: '#0B5CFF' }}
|
||||
suffix="家"
|
||||
|
||||
@@ -122,7 +122,7 @@ export default function SubscriptionsPage() {
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="推荐出海主推方案"
|
||||
title="推荐首选方案"
|
||||
value="Pro 专业商务版"
|
||||
style={{ fontSize: 18, color: '#00B42A' }}
|
||||
/>
|
||||
@@ -144,7 +144,7 @@ export default function SubscriptionsPage() {
|
||||
<Card
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontWeight: 600 }}>SaaS 订阅套餐规格与出海权益矩阵</span>
|
||||
<span style={{ fontWeight: 600 }}>SaaS 订阅套餐规格与全球商务权益矩阵</span>
|
||||
<Tag color="arcoblue" size="small">平台超管可调控</Tag>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ const i18n = {
|
||||
'zh-CN': {
|
||||
'menu.settings.auditLogs': '安全审计',
|
||||
'auditLogs.title': '系统安全审计日志',
|
||||
'auditLogs.subtitle': '追踪管理员与系统关键操作行为链路,满足安全与出海合规要求。',
|
||||
'auditLogs.subtitle': '追踪管理员与系统关键操作行为链路,满足安全与全球合规要求。',
|
||||
'auditLogs.search.keywordPlaceholder': '搜索操作人、行为名称或 IP 地址',
|
||||
'auditLogs.filter.module': '业务模块',
|
||||
'auditLogs.filter.module.all': '全部模块',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* OmniCard AI (CRMmp) - 全平台跨端核心数据契约与 TypeScript 类型体系
|
||||
*
|
||||
* 遵循出海 SaaS 规范、Bento Grid 便当盒美学与 RFC 6350 国际标准。
|
||||
* 遵循现代全球化 SaaS 规范、Bento Grid 便当盒美学与 RFC 6350 国际标准。
|
||||
* 供 OmniCard 后端与管理端共享镜像复用。
|
||||
* 严格类型安全,杜绝 any。
|
||||
*/
|
||||
@@ -117,7 +117,7 @@ export interface BentoBlock {
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 2. 访客雷达与轻量级出海 CRM 契约 (Radar & Lead)
|
||||
// 2. 访客雷达与轻量级商务 CRM 契约 (Radar & Lead)
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
@@ -221,11 +221,15 @@ export interface WorkplaceKpi {
|
||||
export interface WorkplaceRadarItem {
|
||||
id: number | string;
|
||||
cardId: number | string;
|
||||
cardTitle?: string;
|
||||
visitorId?: string;
|
||||
country: string;
|
||||
city: string;
|
||||
touchType: TouchType;
|
||||
stayDurationMs: number;
|
||||
viewedBlocks: string[];
|
||||
intentScore?: number;
|
||||
isLocal?: boolean;
|
||||
createTime: Date | string;
|
||||
}
|
||||
|
||||
@@ -655,7 +659,7 @@ export interface CardArticle {
|
||||
}
|
||||
|
||||
/**
|
||||
* 出海案例作品契约
|
||||
* 标杆案例作品契约
|
||||
*/
|
||||
export interface CardCase {
|
||||
id?: string | number;
|
||||
|
||||
@@ -49,7 +49,7 @@ test('2. ai/concierge 必须遵循设计 Token,禁止 emoji 与硬编码颜色
|
||||
assert.ok(!emojiRegex.test(content), '页面中严禁使用 emoji 图标或字符');
|
||||
|
||||
// 人设切换支持
|
||||
assert.ok(content.includes('trade_expert'), '必须支持外贸专家预设 trade_expert');
|
||||
assert.ok(content.includes('trade_expert'), '必须支持国际贸易顾问预设 trade_expert');
|
||||
assert.ok(content.includes('business_rep'), '必须支持商务代表预设 business_rep');
|
||||
assert.ok(content.includes('tech_advisor'), '必须支持技术顾问预设 tech_advisor');
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ test('1. routes.ts 已注册 card/cases,对应 pages/card/cases', () => {
|
||||
|
||||
test('2. locale 必须包含 menu.card.cases 中英文案', () => {
|
||||
assert.equal(defaultI18n['en-US']['menu.card.cases'], 'Portfolio');
|
||||
assert.equal(defaultI18n['zh-CN']['menu.card.cases'], '出海案例作品集');
|
||||
assert.equal(defaultI18n['zh-CN']['menu.card.cases'], '案例作品集');
|
||||
});
|
||||
|
||||
test('3. cases 页面必须请求 CRUD 接口并遵守 Cool-Admin 协议', () => {
|
||||
|
||||
@@ -82,7 +82,7 @@ test('5. 工作台 i18n 必须覆盖 KPI、雷达动态与快捷入口,且含
|
||||
assert.ok(!EMOJI_RE.test(localeContent), '语言包严禁 emoji');
|
||||
});
|
||||
|
||||
test('6. 专业出海运营官视角:包含全球雷达监控、渠道漏斗与效率工具,全屏仅 1 个主 CTA', () => {
|
||||
test('6. 专业运营官视角:包含全球雷达监控、渠道漏斗与效率工具,全屏仅 1 个主 CTA', () => {
|
||||
const content = fs.readFileSync(pagePath, 'utf-8');
|
||||
const localeContent = fs.readFileSync(localePath, 'utf-8');
|
||||
|
||||
@@ -118,7 +118,7 @@ test('6. 专业出海运营官视角:包含全球雷达监控、渠道漏斗
|
||||
assert.ok(!EMOJI_RE.test(content), '页面代码严禁 emoji');
|
||||
});
|
||||
|
||||
test('7. 出海运营快捷中枢:覆盖8大核心业务快捷入口与运营预警,全屏仅1个主CTA', () => {
|
||||
test('7. 全球运营快捷中枢:覆盖8大核心业务快捷入口与运营预警,全屏仅1个主CTA', () => {
|
||||
const content = fs.readFileSync(pagePath, 'utf-8');
|
||||
const localeContent = fs.readFileSync(localePath, 'utf-8');
|
||||
|
||||
@@ -152,4 +152,37 @@ test('7. 出海运营快捷中枢:覆盖8大核心业务快捷入口与运营
|
||||
assert.ok(!EMOJI_RE.test(content), '页面代码严禁 emoji');
|
||||
});
|
||||
|
||||
test('8. 实时雷达数据专家重构:相对时间、本地环境优雅降级、名片归属与科学意向分分级', () => {
|
||||
const content = fs.readFileSync(pagePath, 'utf-8');
|
||||
const localeContent = fs.readFileSync(localePath, 'utf-8');
|
||||
|
||||
// 1. 相对时间与本地化支持
|
||||
const radarKeys = [
|
||||
'workplace.radar.localEnv',
|
||||
'workplace.radar.justNow',
|
||||
'workplace.radar.minsAgo',
|
||||
'workplace.radar.hoursAgo',
|
||||
'workplace.radar.bounce',
|
||||
'workplace.radar.cardFallback',
|
||||
];
|
||||
for (const key of radarKeys) {
|
||||
assert.ok(
|
||||
localeContent.includes(key),
|
||||
`语言包必须包含雷达数据语义化词条: ${key}`
|
||||
);
|
||||
}
|
||||
|
||||
// 2. 页面中实现相对时间格式化与本地环境优雅降级
|
||||
assert.ok(content.includes('formatRelativeTime'), '必须包含相对时间计算逻辑');
|
||||
assert.ok(content.includes('formatLocation'), '必须包含本地环境优雅降级逻辑');
|
||||
assert.ok(content.includes('formatStayText'), '必须包含跳出/停留文案格式化');
|
||||
assert.ok(content.includes('getIntentTagColor'), '必须包含科学意向分语义色彩分级');
|
||||
|
||||
// 3. 规范约束
|
||||
assert.ok(!EMOJI_RE.test(localeContent), '语言包严禁 emoji');
|
||||
assert.ok(!EMOJI_RE.test(content), '页面代码严禁 emoji');
|
||||
assert.ok(!/\bany\b/.test(content), '严禁使用 any');
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ const REMOVED_DEMO_MODULES = [
|
||||
'user',
|
||||
];
|
||||
|
||||
test('1. 核心出海业务菜单必须保留在侧边栏正常展示', () => {
|
||||
test('1. 核心业务菜单必须保留在侧边栏正常展示', () => {
|
||||
const content = fs.readFileSync(routesPath, 'utf-8');
|
||||
for (const key of CORE_BIZ_MODULES) {
|
||||
assert.ok(
|
||||
|
||||
@@ -126,14 +126,12 @@ test('5. mpuniapp/common/tokens.uts - uni-app x 跨端 Token 校验', () => {
|
||||
assert.ok(content.includes('export const utsRadius: UtsRadius ='));
|
||||
assert.ok(content.includes('export const utsTypography: UtsTypography ='));
|
||||
|
||||
// 校验关键值
|
||||
assert.ok(content.includes("'#0B5CFF'"));
|
||||
assert.ok(content.includes("'#4D8DFF'"));
|
||||
assert.ok(content.includes("'#7C5CFF'"));
|
||||
assert.ok(content.includes("'#9B8CFF'"));
|
||||
assert.ok(content.includes("button: '12px'"));
|
||||
assert.ok(content.includes("card: '16px'"));
|
||||
assert.ok(content.includes("modal: '20px'"));
|
||||
// 校验关键值 (严格遵守 AGENTS.md 权威 Design Token)
|
||||
assert.ok(content.includes("'#0088FF'"));
|
||||
assert.ok(content.includes("'#3894FF'"));
|
||||
assert.ok(content.includes("button: '14px'"));
|
||||
assert.ok(content.includes("card: '20px'"));
|
||||
assert.ok(content.includes("modal: '24px'"));
|
||||
assert.ok(content.includes("space16: '16px'"));
|
||||
});
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ test('3. 快捷导航卡片副标题必须具备单行文本防截断样式', ()
|
||||
);
|
||||
});
|
||||
|
||||
test('4. 零 any,零 emoji,符合出海国际化商务规范', () => {
|
||||
test('4. 零 any,零 emoji,符合国际化现代商务规范', () => {
|
||||
const tsx = read('src/pages/dashboard/workplace/index.tsx');
|
||||
const styles = read('src/pages/dashboard/workplace/style/index.module.less');
|
||||
|
||||
|
||||
40
backend/database/apply-seed.js
普通文件
40
backend/database/apply-seed.js
普通文件
@@ -0,0 +1,40 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mysql = require('mysql2/promise');
|
||||
|
||||
async function run() {
|
||||
const sqlPath = path.join(__dirname, 'seed_cards.sql');
|
||||
const sqlContent = fs.readFileSync(sqlPath, 'utf8');
|
||||
|
||||
const connection = await mysql.createConnection({
|
||||
host: '127.0.0.1',
|
||||
port: 3306,
|
||||
user: 'root',
|
||||
password: '123456',
|
||||
database: 'crmmpdata',
|
||||
multipleStatements: true,
|
||||
charset: 'utf8mb4',
|
||||
});
|
||||
|
||||
try {
|
||||
console.log('[Seed] 正在导入多媒体名片数据 (UTF-8)...');
|
||||
await connection.query(sqlContent);
|
||||
console.log('[Seed] 5 张高保真多媒体名片数据导入成功!');
|
||||
|
||||
// 打印验证统计
|
||||
const [cards] = await connection.query('SELECT id, slug, name, company FROM card_info WHERE id >= 1001');
|
||||
console.log(`[Seed] 成功验证 ${cards.length} 张名片:`);
|
||||
cards.forEach(c => console.log(` - [${c.id}] ${c.name} (${c.company}) -> /${c.slug}`));
|
||||
|
||||
const [videos] = await connection.query("SELECT id, card_id, title FROM card_bento WHERE type = 'video'");
|
||||
console.log(`[Seed] 成功验证 ${videos.length} 个视频 Bento 块:`);
|
||||
videos.forEach(v => console.log(` - [Bento #${v.id}] (名片 #${v.card_id}): ${v.title}`));
|
||||
} catch (err) {
|
||||
console.error('[Seed Error]', err);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
287
backend/database/seed_cards.sql
普通文件
287
backend/database/seed_cards.sql
普通文件
@@ -0,0 +1,287 @@
|
||||
-- ====================================================================
|
||||
-- OmniCard AI (CRMmp) - 全球多媒体智能名片种子数据 (配图与配视频)
|
||||
-- 包含 5 组覆盖全球主流行业的真实高保真名片、Bento Grid 图文与视频组件、案例及文章
|
||||
-- 适用数据库: crmmpdata
|
||||
-- ====================================================================
|
||||
|
||||
-- 1. 清理已有 Mock 名片数据(避免重复插入报错,支持幂等执行)
|
||||
DELETE FROM `card_article` WHERE `card_id` IN (1001, 1002, 1003, 1004, 1005);
|
||||
DELETE FROM `card_cases` WHERE `card_id` IN (1001, 1002, 1003, 1004, 1005);
|
||||
DELETE FROM `card_bento` WHERE `card_id` IN (1001, 1002, 1003, 1004, 1005);
|
||||
DELETE FROM `card_info` WHERE `id` IN (1001, 1002, 1003, 1004, 1005);
|
||||
|
||||
-- --------------------------------------------------------------------
|
||||
-- 2. 插入名片主表数据 (card_info)
|
||||
-- --------------------------------------------------------------------
|
||||
INSERT INTO `card_info` (
|
||||
`id`, `tenant_id`, `slug`, `name`, `title`, `company`, `slogan`,
|
||||
`avatar_url`, `phone`, `email`, `whatsapp`, `linkedin`, `twitter`, `wechat`,
|
||||
`timezone`, `audio_pronunciation_url`, `status`, `theme_code`, `user_id`, `remark`,
|
||||
`create_time`, `update_time`
|
||||
) VALUES
|
||||
-- 名片 1: 消费电子与全球智能硬件 (Alex Zheng)
|
||||
(
|
||||
1001, 1, 'alex-zheng', 'Alex Zheng', 'VP of Global Expansion', 'OmniCard Global Inc.',
|
||||
'Empowering Global Hardware Brands with Next-Gen Intelligence',
|
||||
'https://images.unsplash.com/photo-1534528741775-53994a69daeb.jpg',
|
||||
'+8613800138001', 'alex.zheng@omnicard.ai', '+8613800138001',
|
||||
'https://linkedin.com/in/alex-zheng-omni', 'https://x.com/alexzheng_hw', NULL,
|
||||
'Asia/Singapore', 'https://cdn.omnicard.ai/audio/alex-zheng.mp3',
|
||||
'published', 'corporate_navy', 101, '全球智能硬件领军高管,CES 2026 演讲嘉宾',
|
||||
NOW(), NOW()
|
||||
),
|
||||
-- 名片 2: 全球云原生 & AI 基础设施 (Elena Rostova)
|
||||
(
|
||||
1002, 1, 'elena-rostova', 'Elena Rostova', 'Chief Cloud Architect', 'NebulaScale Cloud AI',
|
||||
'Building Resilient Multi-Cloud AI Infrastructures Globally',
|
||||
'https://images.unsplash.com/photo-1580489944761-15a19d654956.jpg',
|
||||
'+14155552671', 'elena.rostova@nebulascale.io', '+14155552671',
|
||||
'https://linkedin.com/in/elena-rostova-cloud', 'https://x.com/elena_infra', NULL,
|
||||
'America/Los_Angeles', 'https://cdn.omnicard.ai/audio/elena-rostova.mp3',
|
||||
'published', 'tech_dark', 102, '硅谷云原生基础设施领军专家',
|
||||
NOW(), NOW()
|
||||
),
|
||||
-- 名片 3: 跨国智慧能源与技术合作 (Tariq Al-Mansoor)
|
||||
(
|
||||
1003, 1, 'tariq-al-mansoor', 'Tariq Al-Mansoor', 'MENA Business Director', 'SilkRoad Oasis Ventures',
|
||||
'Bridging APAC Technologies with Middle East Energy Infrastructure',
|
||||
'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d.jpg',
|
||||
'+971501234567', 'tariq@silkroadoasis.ae', '+971501234567',
|
||||
'https://linkedin.com/in/tariq-almansoor-mena', NULL, NULL,
|
||||
'Asia/Dubai', 'https://cdn.omnicard.ai/audio/tariq.mp3',
|
||||
'published', 'gold_luxe', 103, '海湾地区重大清洁能源国际合作项目投资总监',
|
||||
NOW(), NOW()
|
||||
),
|
||||
-- 名片 4: 全球 DTC 时尚与可持续设计 (Chloe Dubois)
|
||||
(
|
||||
1004, 1, 'chloe-dubois', 'Chloe Dubois', 'Global Brand Officer', 'Atelier Lumina Paris',
|
||||
'Sustainable Luxury & Digital Craftsmanship for Conscious Consumers',
|
||||
'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2.jpg',
|
||||
'+33612345678', 'chloe.dubois@atelierlumina.fr', '+33612345678',
|
||||
'https://linkedin.com/in/chloe-dubois-luxury', NULL, NULL,
|
||||
'Europe/Paris', 'https://cdn.omnicard.ai/audio/chloe.mp3',
|
||||
'published', 'aurora_glow', 104, '巴黎可持续时尚与国际设计师品牌主理人',
|
||||
NOW(), NOW()
|
||||
),
|
||||
-- 名片 5: 跨国供应链与自动化物流 (Kenji Tanaka)
|
||||
(
|
||||
1005, 1, 'kenji-tanaka', 'Kenji Tanaka', 'APAC Logistics VP', 'TransPacific AutoLink',
|
||||
'Optimizing Next-Gen Cross-Border Automotive Supply Chains',
|
||||
'https://images.unsplash.com/photo-1500648767791-00dcc994a43e.jpg',
|
||||
'+81312345678', 'tanaka.kenji@transpacific.jp', '+81312345678',
|
||||
'https://linkedin.com/in/kenji-tanaka-logistics', NULL, 'kenji_autolink',
|
||||
'Asia/Tokyo', 'https://cdn.omnicard.ai/audio/tanaka.mp3',
|
||||
'published', 'emerald_fresh', 105, '日韩与东南亚跨国汽车主机厂一级供应链负责人',
|
||||
NOW(), NOW()
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------------------
|
||||
-- 3. 插入 Bento Grid 网格块组件 (card_bento) 含配图与视频
|
||||
-- --------------------------------------------------------------------
|
||||
INSERT INTO `card_bento` (
|
||||
`id`, `card_id`, `type`, `col_span`, `row_span`, `sort_order`,
|
||||
`title`, `content`, `media_url`, `link_url`, `meta_json`, `status`,
|
||||
`create_time`, `update_time`
|
||||
) VALUES
|
||||
-- 卡片 1 (Alex Zheng) Bento 块
|
||||
(
|
||||
2001, 1001, 'hero', 2, 1, 1,
|
||||
'关于我与 OmniCard', '专注全球物联网与智能消费电子 12 年,连接全球 60+ 国家和地区的创新生态。',
|
||||
'https://images.unsplash.com/photo-1497366216548-37526070297c.jpg', NULL, NULL, 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
2002, 1001, 'video', 2, 2, 2,
|
||||
'OmniCard Next 旗舰实体卡实机开箱演示', '超精密阳极氧化铝雕刻,内置双频高灵敏 NFC 芯片与动态感应天线,轻触即达微官网。',
|
||||
'https://images.unsplash.com/photo-1519389950473-47ba0277781c.jpg',
|
||||
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4',
|
||||
'{"videoUrl":"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4","poster":"https://images.unsplash.com/photo-1519389950473-47ba0277781c.jpg","duration":"01:28","aspectRatio":"16:9","title":"OmniCard Next 硬件工艺与触卡交互"}',
|
||||
1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
2003, 1001, 'case', 1, 1, 3,
|
||||
'GITEX 迪拜展数字化留资', '实现 10,000+ 国际买家 0.5 秒即时触卡交互,获客转化提升 320%。',
|
||||
'https://images.unsplash.com/photo-1511578314322-379afb476865.jpg', NULL, NULL, 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
2004, 1001, 'article', 1, 1, 4,
|
||||
'2026 全球智能硬件合规与准入白皮书', '深度解读 CE/FCC/KC 认证新规与国际电池环保法规应对策略。',
|
||||
'https://images.unsplash.com/photo-1451187580459-43490279c0fa.jpg', NULL, NULL, 1, NOW(), NOW()
|
||||
),
|
||||
|
||||
-- 卡片 2 (Elena Rostova) Bento 块
|
||||
(
|
||||
2005, 1002, 'hero', 2, 1, 1,
|
||||
'全球云架构与大模型算力调度', '主导超过 5,000+ 节点高可用 Kubernetes 集群与跨大洲大模型推理加速网络设计。',
|
||||
'https://images.unsplash.com/photo-1558494949-ef010cbdcc31.jpg', NULL, NULL, 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
2006, 1002, 'video', 2, 2, 2,
|
||||
'NebulaScale 分布式大模型推理架构演示', '实时观测跨欧美亚太算力流转与动态故障转移,端到端延迟压至 35ms 以内。',
|
||||
'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5.jpg',
|
||||
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
||||
'{"videoUrl":"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4","poster":"https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5.jpg","duration":"02:45","aspectRatio":"16:9","title":"跨国高可用集群实时压测演练"}',
|
||||
1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
2007, 1002, 'case', 1, 1, 3,
|
||||
'FinTech 全球秒级支付架构重构', '99.999% 可用性保障,支撑每日 3 亿美元全球交易量。',
|
||||
'https://images.unsplash.com/photo-1563986768609-322da13575f3.jpg', NULL, NULL, 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
2008, 1002, 'article', 1, 1, 4,
|
||||
'现代微服务下的多云零信任安全实践', '解构全球分布式企业如何通过 eBPF 与 SPIFFE 构建坚固的应用安全边界。',
|
||||
'https://images.unsplash.com/photo-1504384308090-c894fdcc538d.jpg', NULL, NULL, 1, NOW(), NOW()
|
||||
),
|
||||
|
||||
-- 卡片 3 (Tariq Al-Mansoor) Bento 块
|
||||
(
|
||||
2009, 1003, 'hero', 2, 1, 1,
|
||||
'海湾合作委员会 (GCC) 绿色能源生态', '常驻阿布扎比与利雅得,深耕沙特“2030愿景”与国际绿色能源技术对接。',
|
||||
'https://images.unsplash.com/photo-1509391365360-2e959784a276.jpg', NULL, NULL, 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
2010, 1003, 'video', 2, 2, 2,
|
||||
'阿布扎比 2GW 光伏产业示范园落成实景', '沙漠极端高温环境下的自动化清扫与高效储能实景纪录片,见证绿色能源奇迹。',
|
||||
'https://images.unsplash.com/photo-1497435334941-8c899ee9e8e9.jpg',
|
||||
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4',
|
||||
'{"videoUrl":"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4","poster":"https://images.unsplash.com/photo-1497435334941-8c899ee9e8e9.jpg","duration":"02:10","aspectRatio":"16:9","title":"中东大型绿电产业园实况航拍"}',
|
||||
1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
2011, 1003, 'case', 1, 1, 3,
|
||||
'沙特 NEOM 新城微电网建设', '联合国际领先储能集团完成全离网微电网调试,供电可靠率达 99.98%。',
|
||||
'https://images.unsplash.com/photo-1466611653911-95081537e5b7.jpg', NULL, NULL, 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
2012, 1003, 'article', 1, 1, 4,
|
||||
'海湾国家商业投资环境与法律合规指引', '从外资持股比例到本地化雇佣比率,全方位规避国际投资法律风险。',
|
||||
'https://images.unsplash.com/photo-1512453979798-5ea266f8880c.jpg', NULL, NULL, 1, NOW(), NOW()
|
||||
),
|
||||
|
||||
-- 卡片 4 (Chloe Dubois) Bento 块
|
||||
(
|
||||
2013, 1004, 'hero', 2, 1, 1,
|
||||
'可持续奢品与数字美学设计', '联合欧洲独立设计师与亚洲循环新材料实验室,打造触动全球年轻一代的现代胶囊衣橱。',
|
||||
'https://images.unsplash.com/photo-1490481651871-ab68de25d43d.jpg', NULL, NULL, 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
2014, 1004, 'video', 2, 2, 2,
|
||||
'巴黎绿色时尚周品牌秀场精选', '从可降解生物丝绸到 0 碳足迹工坊生产实拍,展现先锋环保奢品背后的匠心。',
|
||||
'https://images.unsplash.com/photo-1469334031218-e382a71b716b.jpg',
|
||||
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4',
|
||||
'{"videoUrl":"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4","poster":"https://images.unsplash.com/photo-1469334031218-e382a71b716b.jpg","duration":"01:50","aspectRatio":"16:9","title":"Atelier Lumina Paris 2026 秀场全纪录"}',
|
||||
1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
2015, 1004, 'case', 1, 1, 3,
|
||||
'欧美 DTC 独立站零碳系列发售', '上线 48 小时首发即罄,Instagram 话题曝光突破 3,500 万次。',
|
||||
'https://images.unsplash.com/photo-1441986300917-64674bd600d8.jpg', NULL, NULL, 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
2016, 1004, 'article', 1, 1, 4,
|
||||
'时尚产业的数字护照 (DPP) 全球实践', '如何利用区块链与 NFC 芯片追踪服装从棉花田到衣橱的全生命周期。',
|
||||
'https://images.unsplash.com/photo-1558769132-cb1aea458c5e.jpg', NULL, NULL, 1, NOW(), NOW()
|
||||
),
|
||||
|
||||
-- 卡片 5 (Kenji Tanaka) Bento 块
|
||||
(
|
||||
2017, 1005, 'hero', 2, 1, 1,
|
||||
'跨国汽车产业链智能物流中枢', '管理东京、上海、曼谷三大核心转运港口与 12 座保税自动化立体中心仓。',
|
||||
'https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d.jpg', NULL, NULL, 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
2018, 1005, 'video', 2, 2, 2,
|
||||
'跨国自动化立体仓储 AGV 机器人穿梭实况', '全天候智能算法调度,实现零差错零部件入库与跨国干线精准集拼装船。',
|
||||
'https://images.unsplash.com/photo-1616401784845-180882ba9ba8.jpg',
|
||||
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4',
|
||||
'{"videoUrl":"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4","poster":"https://images.unsplash.com/photo-1616401784845-180882ba9ba8.jpg","duration":"01:35","aspectRatio":"16:9","title":"跨国现代化智能立体仓全景实拍"}',
|
||||
1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
2019, 1005, 'case', 1, 1, 3,
|
||||
'中日新能源车配件 48 小时达', '通过全链条条码及 RFID 联动,清关及通关提速 60%。',
|
||||
'https://images.unsplash.com/photo-1578575437130-527eed3abbec.jpg', NULL, NULL, 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
2020, 1005, 'article', 1, 1, 4,
|
||||
'区域全面经济伙伴关系协定 (RCEP) 关务实战指南', '深度解读原产地累积规则与跨国物流链条中的税费优化策略。',
|
||||
'https://images.unsplash.com/photo-1542744173-8e7e53415bb0.jpg', NULL, NULL, 1, NOW(), NOW()
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------------------
|
||||
-- 4. 插入标杆案例表数据 (card_cases)
|
||||
-- --------------------------------------------------------------------
|
||||
INSERT INTO `card_cases` (
|
||||
`id`, `card_id`, `title`, `client`, `duration`, `metrics`,
|
||||
`cover_image`, `pdf_url`, `description`, `sort_order`,
|
||||
`create_time`, `update_time`
|
||||
) VALUES
|
||||
(
|
||||
3001, 1001, 'GITEX 迪拜展会智能名片规模化交付', '阿联酋国际商业联合会', '2025.10 - 2025.11',
|
||||
'触卡 12,500+ 次,沉淀高意向商业线索 890 条',
|
||||
'https://images.unsplash.com/photo-1511578314322-379afb476865.jpg',
|
||||
'https://cdn.omnicard.ai/cases/gitex-2025-report.pdf',
|
||||
'为 48 家国际参展代表团统一配置智能名片与雷达跟进中枢,完成全程数字化展会获客。', 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
3002, 1002, '跨国全球支付金融级容灾架构改造', 'PayNova International', '2025.03 - 2025.08',
|
||||
'交易超时率降低 98.4%,算力成本节省 35%',
|
||||
'https://images.unsplash.com/photo-1563986768609-322da13575f3.jpg', NULL,
|
||||
'基于异地多活方案实现 RPO=0, RTO<1s 的高可用架构,顺利通过全球多国金融安全审计。', 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
3003, 1003, '阿布扎比综合智慧能源示范中心一期', 'Abu Dhabi Future Energy PJSC', '2024.08 - 2025.04',
|
||||
'年发电量 32 亿千瓦时,减排二氧化碳 240 万吨',
|
||||
'https://images.unsplash.com/photo-1466611653911-95081537e5b7.jpg', NULL,
|
||||
'整合国际先锋光伏组件与智能追踪支架,打造跨国绿色能源合作的典范工程。', 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
3004, 1004, 'Atelier Lumina 欧美全渠道首发战役', 'Lumina Paris', '2025.05 - 2025.09',
|
||||
'欧美 GMV 增长 210%,复购率达到 38.6%',
|
||||
'https://images.unsplash.com/photo-1441986300917-64674bd600d8.jpg', NULL,
|
||||
'通过数字体验名片与 VIP 专属私人定制预约通道,构建高端时尚圈层闭环。', 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
3005, 1005, '亚太区域核心零部件保税中转枢纽项目', 'Global AutoTech Consortium', '2024.11 - 2025.07',
|
||||
'仓储周转效率提高 42%,破损率控制在 0.001% 以下',
|
||||
'https://images.unsplash.com/photo-1578575437130-527eed3abbec.jpg', NULL,
|
||||
'成功构建日韩到东盟的跨国零备件绿色走廊,达成准时化(JIT)精益交付。', 1, NOW(), NOW()
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------------------
|
||||
-- 5. 插入深度文章与洞察表数据 (card_article)
|
||||
-- --------------------------------------------------------------------
|
||||
INSERT INTO `card_article` (
|
||||
`id`, `card_id`, `title`, `summary`, `content`, `cover_image`,
|
||||
`is_published`, `create_time`, `update_time`
|
||||
) VALUES
|
||||
(
|
||||
4001, 1001, '智能硬件全球化新范式:从单品突围到全域生态触达',
|
||||
'探讨如何结合 NFC 与端侧 AI 技术在国际展会和商务洽谈中建立第一分钟信任。',
|
||||
'在全球消费电子产业加速演进的今天,传统的纸质物料正在迅速被具备实时交互能力的数字名片和微官网替代。在 CES、IFA、GITEX 等大型展会现场,商务人员递出的不再是一张单薄的卡纸,而是一个聚合了产品全景演示、合规资质、多语言视频与 AI 顾问的微型展厅...',
|
||||
'https://images.unsplash.com/photo-1451187580459-43490279c0fa.jpg', 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
4002, 1002, '在大模型时代重新思考全球分布式架构演进',
|
||||
'探索算力调度从传统容器编排向 GPU 异构资源池化迁移的核心挑战。',
|
||||
'伴随企业全球化战略加速,算力孤岛与网络延迟成为制约 AI 商业落地的最大瓶颈。本文结合我们在跨大洲大模型推理加速网络设计中的实践,系统阐述如何利用 eBPF 网络加速、智能边缘缓存与拓扑感知调度,将端到端跨洲请求延迟压缩至极限...',
|
||||
'https://images.unsplash.com/photo-1504384308090-c894fdcc538d.jpg', 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
4003, 1003, '走进中东:沙特与阿联酋新一轮科技能源投资机遇',
|
||||
'解析全球高科技与能源装备企业在海湾市场的本土化突围之道。',
|
||||
'随着海湾国家加速推进“2030愿景”,以绿色能源、海水淡化与智慧城市为核心的万亿级基础设施建设全面铺开。国际企业深耕中东正经历从单纯“设备供应”向“资本+技术+本土化运营”深度模式的迭代升级...',
|
||||
'https://images.unsplash.com/photo-1512453979798-5ea266f8880c.jpg', 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
4004, 1004, '为什么新一代全球 DTC 品牌需要重塑品牌心智',
|
||||
'摆脱流量依赖与单一价格战,以故事叙事和品牌价值赢得国际高净值客群。',
|
||||
'全球品牌的竞争已经从单纯的供应链红利转向了文化叙事与品牌价值主张的博弈。在国际主流市场,年轻消费者越来越看重品牌的环保倡议、透明供应链与数字化体验。通过 NFC 数字衣标与沉浸式便当盒微官网,品牌得以与消费者建立长期信任关系...',
|
||||
'https://images.unsplash.com/photo-1558769132-cb1aea458c5e.jpg', 1, NOW(), NOW()
|
||||
),
|
||||
(
|
||||
4005, 1005, '全球地缘与贸易新常态下的弹性供应链构建',
|
||||
'面对跨国运价波动与港口堵塞风险,如何通过数字孪生提升全球业务韧性。',
|
||||
'跨国供应链的制胜关键已经从单一的价格竞争转向全链路的可视化与动态避险能力。通过引入 AGV 自动化立体仓储系统、跨国关务自动化预申报与 RFID 实时追踪技术,亚太区域的零部件周转效率获得了质的飞跃...',
|
||||
'https://images.unsplash.com/photo-1542744173-8e7e53415bb0.jpg', 1, NOW(), NOW()
|
||||
);
|
||||
@@ -61,6 +61,7 @@
|
||||
"lint": "mwts check",
|
||||
"lint:fix": "mwts fix",
|
||||
"ci": "npm run cov",
|
||||
"seed:cards": "node database/apply-seed.js",
|
||||
"build": "cool entity && bundle && mwtsc --cleanOutDir",
|
||||
"build:obfuscate": "cool entity && bundle && mwtsc --cleanOutDir && cool obfuscate",
|
||||
"pkg": "rimraf build && mkdir build && npm run build && pkg . -d > build/pkg.log",
|
||||
|
||||
@@ -175,7 +175,7 @@ const COUNTRY_UTC_OFFSETS: Record<string, number> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 出海客户最佳商务联系时间窗口计算
|
||||
* 全球客户最佳商务联系时间窗口计算
|
||||
*/
|
||||
export function resolveBestContactWindow(
|
||||
countryCodeOrName?: string,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* OmniCard AI (CRMmp) - 全平台跨端核心数据契约与 TypeScript 类型体系
|
||||
*
|
||||
* 遵循出海 SaaS 规范、Bento Grid 便当盒美学与 RFC 6350 国际标准。
|
||||
* 遵循现代全球化 SaaS 规范、Bento Grid 便当盒美学与 RFC 6350 国际标准。
|
||||
* 供 OmniCard 后端与管理端共享镜像复用。
|
||||
* 严格类型安全,杜绝 any。
|
||||
*/
|
||||
@@ -123,7 +123,7 @@ export interface BentoBlock {
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 2. 访客雷达与轻量级出海 CRM 契约 (Radar & Lead)
|
||||
// 2. 访客雷达与轻量级商务 CRM 契约 (Radar & Lead)
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
@@ -324,11 +324,15 @@ export interface WorkplaceKpi {
|
||||
export interface WorkplaceRadarItem {
|
||||
id: number | string;
|
||||
cardId: number | string;
|
||||
cardTitle?: string;
|
||||
visitorId?: string;
|
||||
country: string;
|
||||
city: string;
|
||||
touchType: TouchType;
|
||||
stayDurationMs: number;
|
||||
viewedBlocks: string[];
|
||||
intentScore?: number;
|
||||
isLocal?: boolean;
|
||||
createTime: Date | string;
|
||||
}
|
||||
|
||||
@@ -856,7 +860,7 @@ export interface CardArticle {
|
||||
}
|
||||
|
||||
/**
|
||||
* 出海案例作品契约
|
||||
* 标杆案例作品契约
|
||||
*/
|
||||
export interface CardCase {
|
||||
id?: string | number;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ModuleConfig } from '@cool-midway/core';
|
||||
|
||||
/**
|
||||
* AI 顾问与出海润色模块配置
|
||||
* AI 顾问与多语言本地化润色模块配置
|
||||
*/
|
||||
export default () => {
|
||||
return {
|
||||
name: 'AI 智能中枢模块',
|
||||
description: '名片内置顾问对话、意向识别与出海文案润色',
|
||||
description: '名片内置顾问对话、意向识别与多语言文案润色',
|
||||
middlewares: [],
|
||||
globalMiddlewares: [],
|
||||
order: 40,
|
||||
|
||||
@@ -2,18 +2,18 @@ import { CoolController, BaseController } from '@cool-midway/core';
|
||||
import { AiLocalizeService } from '../../service/localize';
|
||||
|
||||
/**
|
||||
* 后台出海润色配置(不开标准 CRUD)
|
||||
* 后台多语言本地化润色配置(不开标准 CRUD)
|
||||
*/
|
||||
export const adminAiLocalizeCurdOption = {
|
||||
api: [] as Array<
|
||||
'add' | 'delete' | 'update' | 'info' | 'list' | 'page'
|
||||
>,
|
||||
service: AiLocalizeService,
|
||||
serviceApis: [{ method: 'localize', summary: 'AI 出海润色' }],
|
||||
serviceApis: [{ method: 'localize', summary: 'AI 本地化润色' }],
|
||||
};
|
||||
|
||||
/**
|
||||
* AI 模块-后台出海润色
|
||||
* AI 模块-后台多语言本地化润色
|
||||
* 路由前缀:/admin/ai/localize
|
||||
*/
|
||||
@CoolController(adminAiLocalizeCurdOption)
|
||||
|
||||
@@ -19,9 +19,9 @@ const PERSONA_PRESETS: Record<
|
||||
trade_expert: {
|
||||
personaType: 'trade_expert',
|
||||
welcomeMsg:
|
||||
'您好!我是您的专属出海业务顾问。请问有什么全球贸易或合作需求可以协助您?',
|
||||
'您好!我是您的专属商务顾问。请问有什么业务咨询或国际合作需求可以协助您?',
|
||||
promptTemplate:
|
||||
'你是一名深耕国际贸易与跨国商务的出海业务专家。用专业、沉稳的商务语调为访客解答产品出海、跨境结算、供应链合作等咨询。引导有明确意向的客商提供联系方式。',
|
||||
'你是一名深耕全球商务与跨国合作的业务顾问。用专业、沉稳的商务语调为访客解答业务合作、跨国结算、供应链方案等咨询。引导有明确意向的客商提供联系方式。',
|
||||
toneStyle: 'professional',
|
||||
triggerLeadThreshold: 70,
|
||||
isEnabled: true,
|
||||
|
||||
@@ -227,7 +227,7 @@ export class DeepSeekService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装出海文案润色提示词
|
||||
* 组装商务文案本地化润色提示词
|
||||
*/
|
||||
buildLocalizePrompt(
|
||||
text: string,
|
||||
@@ -236,7 +236,7 @@ export class DeepSeekService extends BaseService {
|
||||
glossaryTerms?: string[]
|
||||
): string {
|
||||
const lines = [
|
||||
`请将以下商务文本改写/润色为符合目标语言(${targetLocale})与目标语调(${tone})的地道出海商业文案。`,
|
||||
`请将以下商务文本改写/润色为符合目标语言(${targetLocale})与目标语调(${tone})的地道国际商业文案。`,
|
||||
'要求:语义准确地道,符合国际商业习惯,禁止机械直译。',
|
||||
];
|
||||
|
||||
@@ -257,9 +257,9 @@ export class DeepSeekService extends BaseService {
|
||||
tracks?: Array<{ module?: string; duration?: number }>
|
||||
): string {
|
||||
const lines = [
|
||||
'你是一名资深出海销售战略顾问。请根据以下潜在客户画像与其在名片上的浏览行为轨迹,生成一段极具针对性的 WhatsApp/Email 商务破冰首语建议。',
|
||||
'你是一名资深国际商务拓展顾问。请根据以下潜在客户画像与其在名片上的浏览行为轨迹,生成一段极具针对性的 WhatsApp/Email 商务破冰首语建议。',
|
||||
`潜客姓名: ${lead?.name || '客商'}`,
|
||||
`所属公司: ${lead?.company || '出海企业'}`,
|
||||
`所属公司: ${lead?.company || '目标企业'}`,
|
||||
`核心诉求: ${lead?.demand || '商务洽谈'}`,
|
||||
];
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const TONES: ReadonlySet<LocalizeTone> = new Set([
|
||||
]);
|
||||
|
||||
/**
|
||||
* 出海文案润色(本地规则骨架,结合行业术语库强行替换)
|
||||
* 商务文案本地化润色(本地规则骨架,结合行业术语库强行替换)
|
||||
*/
|
||||
@Provide()
|
||||
export class AiLocalizeService extends BaseService {
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface OutreachDraftResponse {
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装出海销售跟进提示词
|
||||
* 组装商务拓展跟进提示词
|
||||
*/
|
||||
export function formatOutreachPrompt(req: OutreachDraftRequest): string {
|
||||
const langCode = (req.targetLanguage || 'en').toLowerCase();
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CardCasesEntity } from '../../entity/cases';
|
||||
import { CardCasesService } from '../../service/cases';
|
||||
|
||||
/**
|
||||
* 后台出海案例作品集标准 CRUD 配置
|
||||
* 后台标杆案例作品集标准 CRUD 配置
|
||||
*/
|
||||
export const adminCardCasesCurdOption = {
|
||||
api: ['add', 'delete', 'update', 'info', 'list', 'page'] as Array<
|
||||
@@ -22,7 +22,7 @@ export const adminCardCasesCurdOption = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 名片模块-后台出海案例作品集管理
|
||||
* 名片模块-后台标杆案例作品集管理
|
||||
* 路由前缀:/admin/card/cases
|
||||
*/
|
||||
@CoolController(adminCardCasesCurdOption)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Column, Entity, Index } from 'typeorm';
|
||||
import type { CardCase } from '../../../comm/types/omni';
|
||||
|
||||
/**
|
||||
* 出海案例作品集实体
|
||||
* 标杆案例作品集实体
|
||||
* 映射数据表 card_cases,严格契合 CardCase 跨端数据契约
|
||||
*/
|
||||
@Entity('card_cases')
|
||||
@@ -25,7 +25,7 @@ export class CardCasesEntity extends BaseEntity implements CardCase {
|
||||
|
||||
@Column({
|
||||
name: 'client',
|
||||
comment: '客户/出海品牌名称',
|
||||
comment: '客户/合作品牌名称',
|
||||
length: 100,
|
||||
nullable: true,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,619 @@
|
||||
import { CardInfoEntity } from '../entity/info';
|
||||
import { CardBentoEntity } from '../entity/bento';
|
||||
import { CardCasesEntity } from '../entity/cases';
|
||||
import { CardArticleEntity } from '../entity/article';
|
||||
import type { BentoBlockType, BentoColSpan, BentoRowSpan } from '../../../comm/types/omni';
|
||||
|
||||
/**
|
||||
* 单张名片的数据包定义
|
||||
*/
|
||||
export interface MockCardBundle {
|
||||
card: Partial<CardInfoEntity>;
|
||||
bentoBlocks: Partial<CardBentoEntity>[];
|
||||
cases: Partial<CardCasesEntity>[];
|
||||
articles: Partial<CardArticleEntity>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 5 组不同全球主流行业的高保真多媒体名片数据(含配图与视频)
|
||||
*/
|
||||
export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
// ==========================================
|
||||
// 名片 1: 消费电子与全球智能硬件 (Alex Zheng)
|
||||
// ==========================================
|
||||
{
|
||||
card: {
|
||||
id: 1001,
|
||||
tenantId: 1,
|
||||
slug: 'alex-zheng',
|
||||
name: 'Alex Zheng',
|
||||
title: 'VP of Global Expansion',
|
||||
company: 'OmniCard Global Inc.',
|
||||
slogan: 'Empowering Global Hardware Brands with Next-Gen Intelligence',
|
||||
avatarUrl: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb.jpg',
|
||||
phone: '+8613800138001',
|
||||
email: 'alex.zheng@omnicard.ai',
|
||||
whatsapp: '+8613800138001',
|
||||
linkedin: 'https://linkedin.com/in/alex-zheng-omni',
|
||||
twitter: 'https://x.com/alexzheng_hw',
|
||||
timezone: 'Asia/Singapore',
|
||||
audioPronunciationUrl: 'https://cdn.omnicard.ai/audio/alex-zheng.mp3',
|
||||
status: 'published',
|
||||
themeCode: 'corporate_navy',
|
||||
userId: 101,
|
||||
remark: '全球智能硬件领军高管,CES 2026 演讲嘉宾',
|
||||
},
|
||||
bentoBlocks: [
|
||||
{
|
||||
id: 2001,
|
||||
cardId: 1001,
|
||||
type: 'hero' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 1,
|
||||
title: '关于我与 OmniCard',
|
||||
content: '专注全球物联网与智能消费电子 12 年,连接全球 60+ 国家和地区的创新生态。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1497366216548-37526070297c.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2002,
|
||||
cardId: 1001,
|
||||
type: 'video' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 2 as BentoRowSpan,
|
||||
sortOrder: 2,
|
||||
title: 'OmniCard Next 旗舰实体卡实机开箱演示',
|
||||
content: '超精密阳极氧化铝雕刻,内置双频高灵敏 NFC 芯片与动态感应天线,轻触即达微官网。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1519389950473-47ba0277781c.jpg',
|
||||
linkUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4',
|
||||
metaJson: {
|
||||
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4',
|
||||
poster: 'https://images.unsplash.com/photo-1519389950473-47ba0277781c.jpg',
|
||||
duration: '01:28',
|
||||
aspectRatio: '16:9',
|
||||
title: 'OmniCard Next 硬件工艺与触卡交互',
|
||||
},
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2003,
|
||||
cardId: 1001,
|
||||
type: 'case' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 3,
|
||||
title: 'GITEX 迪拜展数字化留资',
|
||||
content: '实现 10,000+ 国际买家 0.5 秒即时触卡交互,获客转化提升 320%。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1511578314322-379afb476865.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2004,
|
||||
cardId: 1001,
|
||||
type: 'article' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 4,
|
||||
title: '2026 全球智能硬件合规与准入白皮书',
|
||||
content: '深度解读 CE/FCC/KC 认证新规与国际电池环保法规应对策略。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa.jpg',
|
||||
status: 1,
|
||||
},
|
||||
],
|
||||
cases: [
|
||||
{
|
||||
id: 3001,
|
||||
cardId: 1001,
|
||||
title: 'GITEX 迪拜展会智能名片规模化交付',
|
||||
client: '阿联酋国际商业联合会',
|
||||
duration: '2025.10 - 2025.11',
|
||||
metrics: '触卡 12,500+ 次,沉淀高意向商业线索 890 条',
|
||||
coverImage: 'https://images.unsplash.com/photo-1511578314322-379afb476865.jpg',
|
||||
pdfUrl: 'https://cdn.omnicard.ai/cases/gitex-2025-report.pdf',
|
||||
description: '为 48 家国际参展代表团统一配置智能名片与雷达跟进中枢,完成全程数字化展会获客。',
|
||||
},
|
||||
],
|
||||
articles: [
|
||||
{
|
||||
id: 4001,
|
||||
cardId: 1001,
|
||||
title: '智能硬件全球化新范式:从单品突围到全域生态触达',
|
||||
summary: '探讨如何结合 NFC 与端侧 AI 技术在国际展会和商务洽谈中建立第一分钟信任。',
|
||||
content: '在全球消费电子产业加速演进的今天,传统的纸质物料正在迅速被具备实时交互能力的数字名片和微官网替代...',
|
||||
coverImage: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa.jpg',
|
||||
isPublished: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 名片 2: 全球云原生 & AI 基础设施 (Elena Rostova)
|
||||
// ==========================================
|
||||
{
|
||||
card: {
|
||||
id: 1002,
|
||||
tenantId: 1,
|
||||
slug: 'elena-rostova',
|
||||
name: 'Elena Rostova',
|
||||
title: 'Chief Cloud Architect',
|
||||
company: 'NebulaScale Cloud AI',
|
||||
slogan: 'Building Resilient Multi-Cloud AI Infrastructures Globally',
|
||||
avatarUrl: 'https://images.unsplash.com/photo-1580489944761-15a19d654956.jpg',
|
||||
phone: '+14155552671',
|
||||
email: 'elena.rostova@nebulascale.io',
|
||||
whatsapp: '+14155552671',
|
||||
linkedin: 'https://linkedin.com/in/elena-rostova-cloud',
|
||||
twitter: 'https://x.com/elena_infra',
|
||||
timezone: 'America/Los_Angeles',
|
||||
audioPronunciationUrl: 'https://cdn.omnicard.ai/audio/elena-rostova.mp3',
|
||||
status: 'published',
|
||||
themeCode: 'tech_dark',
|
||||
userId: 102,
|
||||
remark: '硅谷云原生基础设施领军专家',
|
||||
},
|
||||
bentoBlocks: [
|
||||
{
|
||||
id: 2005,
|
||||
cardId: 1002,
|
||||
type: 'hero' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 1,
|
||||
title: '全球云架构与大模型算力调度',
|
||||
content: '主导超过 5,000+ 节点高可用 Kubernetes 集群与跨大洲大模型推理加速网络设计。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2006,
|
||||
cardId: 1002,
|
||||
type: 'video' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 2 as BentoRowSpan,
|
||||
sortOrder: 2,
|
||||
title: 'NebulaScale 分布式大模型推理架构演示',
|
||||
content: '实时观测跨欧美亚太算力流转与动态故障转移,端到端延迟压至 35ms 以内。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5.jpg',
|
||||
linkUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
||||
metaJson: {
|
||||
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
||||
poster: 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5.jpg',
|
||||
duration: '02:45',
|
||||
aspectRatio: '16:9',
|
||||
title: '跨国高可用集群实时压测演练',
|
||||
},
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2007,
|
||||
cardId: 1002,
|
||||
type: 'case' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 3,
|
||||
title: 'FinTech 全球秒级支付架构重构',
|
||||
content: '99.999% 可用性保障,支撑每日 3 亿美元全球交易量。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1563986768609-322da13575f3.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2008,
|
||||
cardId: 1002,
|
||||
type: 'article' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 4,
|
||||
title: '现代微服务下的多云零信任安全实践',
|
||||
content: '解构全球分布式企业如何通过 eBPF 与 SPIFFE 构建坚固的应用安全边界。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1504384308090-c894fdcc538d.jpg',
|
||||
status: 1,
|
||||
},
|
||||
],
|
||||
cases: [
|
||||
{
|
||||
id: 3002,
|
||||
cardId: 1002,
|
||||
title: '跨国全球支付金融级容灾架构改造',
|
||||
client: 'PayNova International',
|
||||
duration: '2025.03 - 2025.08',
|
||||
metrics: '交易超时率降低 98.4%,算力成本节省 35%',
|
||||
coverImage: 'https://images.unsplash.com/photo-1563986768609-322da13575f3.jpg',
|
||||
description: '基于异地多活方案实现 RPO=0, RTO<1s 的高可用架构,顺利通过全球多国金融安全审计。',
|
||||
},
|
||||
],
|
||||
articles: [
|
||||
{
|
||||
id: 4002,
|
||||
cardId: 1002,
|
||||
title: '在大模型时代重新思考全球分布式架构演进',
|
||||
summary: '探索算力调度从传统容器编排向 GPU 异构资源池化迁移的核心挑战。',
|
||||
content: '伴随企业全球化战略加速,算力孤岛与网络延迟成为制约 AI 商业落地的最大瓶颈...',
|
||||
coverImage: 'https://images.unsplash.com/photo-1504384308090-c894fdcc538d.jpg',
|
||||
isPublished: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 名片 3: 跨国智慧能源与技术合作 (Tariq Al-Mansoor)
|
||||
// ==========================================
|
||||
{
|
||||
card: {
|
||||
id: 1003,
|
||||
tenantId: 1,
|
||||
slug: 'tariq-al-mansoor',
|
||||
name: 'Tariq Al-Mansoor',
|
||||
title: 'MENA Business Director',
|
||||
company: 'SilkRoad Oasis Ventures',
|
||||
slogan: 'Bridging APAC Technologies with Middle East Energy Infrastructure',
|
||||
avatarUrl: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d.jpg',
|
||||
phone: '+971501234567',
|
||||
email: 'tariq@silkroadoasis.ae',
|
||||
whatsapp: '+971501234567',
|
||||
linkedin: 'https://linkedin.com/in/tariq-almansoor-mena',
|
||||
timezone: 'Asia/Dubai',
|
||||
audioPronunciationUrl: 'https://cdn.omnicard.ai/audio/tariq.mp3',
|
||||
status: 'published',
|
||||
themeCode: 'gold_luxe',
|
||||
userId: 103,
|
||||
remark: '海湾地区重大清洁能源国际合作项目投资总监',
|
||||
},
|
||||
bentoBlocks: [
|
||||
{
|
||||
id: 2009,
|
||||
cardId: 1003,
|
||||
type: 'hero' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 1,
|
||||
title: '海湾合作委员会 (GCC) 绿色能源生态',
|
||||
content: '常驻阿布扎比与利雅得,深耕沙特“2030愿景”与国际绿色能源技术对接。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1509391365360-2e959784a276.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2010,
|
||||
cardId: 1003,
|
||||
type: 'video' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 2 as BentoRowSpan,
|
||||
sortOrder: 2,
|
||||
title: '阿布扎比 2GW 光伏产业示范园落成实景',
|
||||
content: '沙漠极端高温环境下的自动化清扫与高效储能实景纪录片,见证绿色能源奇迹。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1497435334941-8c899ee9e8e9.jpg',
|
||||
linkUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4',
|
||||
metaJson: {
|
||||
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4',
|
||||
poster: 'https://images.unsplash.com/photo-1497435334941-8c899ee9e8e9.jpg',
|
||||
duration: '02:10',
|
||||
aspectRatio: '16:9',
|
||||
title: '中东大型绿电产业园实况航拍',
|
||||
},
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2011,
|
||||
cardId: 1003,
|
||||
type: 'case' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 3,
|
||||
title: '沙特 NEOM 新城微电网建设',
|
||||
content: '联合国际领先储能集团完成全离网微电网调试,供电可靠率达 99.98%。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1466611653911-95081537e5b7.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2012,
|
||||
cardId: 1003,
|
||||
type: 'article' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 4,
|
||||
title: '海湾国家商业投资环境与法律合规指引',
|
||||
content: '从外资持股比例到本地化雇佣比率,全方位规避国际投资法律风险。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1512453979798-5ea266f8880c.jpg',
|
||||
status: 1,
|
||||
},
|
||||
],
|
||||
cases: [
|
||||
{
|
||||
id: 3003,
|
||||
cardId: 1003,
|
||||
title: '阿布扎比综合智慧能源示范中心一期',
|
||||
client: 'Abu Dhabi Future Energy PJSC',
|
||||
duration: '2024.08 - 2025.04',
|
||||
metrics: '年发电量 32 亿千瓦时,减排二氧化碳 240 万吨',
|
||||
coverImage: 'https://images.unsplash.com/photo-1466611653911-95081537e5b7.jpg',
|
||||
description: '整合国际先锋光伏组件与智能追踪支架,打造跨国绿色能源合作的典范工程。',
|
||||
},
|
||||
],
|
||||
articles: [
|
||||
{
|
||||
id: 4003,
|
||||
cardId: 1003,
|
||||
title: '走进中东:沙特与阿联酋新一轮科技能源投资机遇',
|
||||
summary: '解析全球高科技与能源装备企业在海湾市场的本土化突围之道。',
|
||||
content: '随着海湾国家加速摆脱石油依赖,以绿色能源与智能化技术为核心的产业变革正在全面展开...',
|
||||
coverImage: 'https://images.unsplash.com/photo-1512453979798-5ea266f8880c.jpg',
|
||||
isPublished: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 名片 4: 全球 DTC 时尚与可持续设计 (Chloe Dubois)
|
||||
// ==========================================
|
||||
{
|
||||
card: {
|
||||
id: 1004,
|
||||
tenantId: 1,
|
||||
slug: 'chloe-dubois',
|
||||
name: 'Chloe Dubois',
|
||||
title: 'Global Brand Officer',
|
||||
company: 'Atelier Lumina Paris',
|
||||
slogan: 'Sustainable Luxury & Digital Craftsmanship for Conscious Consumers',
|
||||
avatarUrl: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2.jpg',
|
||||
phone: '+33612345678',
|
||||
email: 'chloe.dubois@atelierlumina.fr',
|
||||
whatsapp: '+33612345678',
|
||||
linkedin: 'https://linkedin.com/in/chloe-dubois-luxury',
|
||||
timezone: 'Europe/Paris',
|
||||
audioPronunciationUrl: 'https://cdn.omnicard.ai/audio/chloe.mp3',
|
||||
status: 'published',
|
||||
themeCode: 'aurora_glow',
|
||||
userId: 104,
|
||||
remark: '巴黎可持续时尚与国际设计师品牌主理人',
|
||||
},
|
||||
bentoBlocks: [
|
||||
{
|
||||
id: 2013,
|
||||
cardId: 1004,
|
||||
type: 'hero' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 1,
|
||||
title: '可持续奢品与数字美学设计',
|
||||
content: '联合欧洲独立设计师与亚洲循环新材料实验室,打造触动全球年轻一代的现代胶囊衣橱。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1490481651871-ab68de25d43d.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2014,
|
||||
cardId: 1004,
|
||||
type: 'video' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 2 as BentoRowSpan,
|
||||
sortOrder: 2,
|
||||
title: '巴黎绿色时尚周品牌秀场精选',
|
||||
content: '从可降解生物丝绸到 0 碳足迹工坊生产实拍,展现先锋环保奢品背后的匠心。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1469334031218-e382a71b716b.jpg',
|
||||
linkUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4',
|
||||
metaJson: {
|
||||
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4',
|
||||
poster: 'https://images.unsplash.com/photo-1469334031218-e382a71b716b.jpg',
|
||||
duration: '01:50',
|
||||
aspectRatio: '16:9',
|
||||
title: 'Atelier Lumina Paris 2026 秀场全纪录',
|
||||
},
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2015,
|
||||
cardId: 1004,
|
||||
type: 'case' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 3,
|
||||
title: '欧美 DTC 独立站零碳系列发售',
|
||||
content: '上线 48 小时首发即罄,Instagram 话题曝光突破 3,500 万次。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1441986300917-64674bd600d8.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2016,
|
||||
cardId: 1004,
|
||||
type: 'article' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 4,
|
||||
title: '时尚产业的数字护照 (DPP) 全球实践',
|
||||
content: '如何利用区块链与 NFC 芯片追踪服装从棉花田到衣橱的全生命周期。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1558769132-cb1aea458c5e.jpg',
|
||||
status: 1,
|
||||
},
|
||||
],
|
||||
cases: [
|
||||
{
|
||||
id: 3004,
|
||||
cardId: 1004,
|
||||
title: 'Atelier Lumina 欧美全渠道首发战役',
|
||||
client: 'Lumina Paris',
|
||||
duration: '2025.05 - 2025.09',
|
||||
metrics: '欧美 GMV 增长 210%,复购率达到 38.6%',
|
||||
coverImage: 'https://images.unsplash.com/photo-1441986300917-64674bd600d8.jpg',
|
||||
description: '通过数字体验名片与 VIP 专属私人定制预约通道,构建高端时尚圈层闭环。',
|
||||
},
|
||||
],
|
||||
articles: [
|
||||
{
|
||||
id: 4004,
|
||||
cardId: 1004,
|
||||
title: '为什么新一代全球 DTC 品牌需要重塑品牌心智',
|
||||
summary: '摆脱流量依赖与单一价格战,以故事叙事和品牌价值赢得国际高净值客群。',
|
||||
content: '全球品牌的竞争已经从单纯的供应链红利转向了文化叙事与品牌价值主张的博弈...',
|
||||
coverImage: 'https://images.unsplash.com/photo-1558769132-cb1aea458c5e.jpg',
|
||||
isPublished: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 名片 5: 跨国供应链与自动化物流 (Kenji Tanaka)
|
||||
// ==========================================
|
||||
{
|
||||
card: {
|
||||
id: 1005,
|
||||
tenantId: 1,
|
||||
slug: 'kenji-tanaka',
|
||||
name: 'Kenji Tanaka',
|
||||
title: 'APAC Logistics VP',
|
||||
company: 'TransPacific AutoLink',
|
||||
slogan: 'Optimizing Next-Gen Cross-Border Automotive Supply Chains',
|
||||
avatarUrl: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e.jpg',
|
||||
phone: '+81312345678',
|
||||
email: 'tanaka.kenji@transpacific.jp',
|
||||
whatsapp: '+81312345678',
|
||||
linkedin: 'https://linkedin.com/in/kenji-tanaka-logistics',
|
||||
wechat: 'kenji_autolink',
|
||||
timezone: 'Asia/Tokyo',
|
||||
audioPronunciationUrl: 'https://cdn.omnicard.ai/audio/tanaka.mp3',
|
||||
status: 'published',
|
||||
themeCode: 'emerald_fresh',
|
||||
userId: 105,
|
||||
remark: '日韩与东南亚跨国汽车主机厂一级供应链负责人',
|
||||
},
|
||||
bentoBlocks: [
|
||||
{
|
||||
id: 2017,
|
||||
cardId: 1005,
|
||||
type: 'hero' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 1,
|
||||
title: '跨国汽车产业链智能物流中枢',
|
||||
content: '管理东京、上海、曼谷三大核心转运港口与 12 座保税自动化立体中心仓。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2018,
|
||||
cardId: 1005,
|
||||
type: 'video' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 2 as BentoRowSpan,
|
||||
sortOrder: 2,
|
||||
title: '跨国自动化立体仓储 AGV 机器人穿梭实况',
|
||||
content: '全天候智能算法调度,实现零差错零部件入库与跨国干线精准集拼装船。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1616401784845-180882ba9ba8.jpg',
|
||||
linkUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4',
|
||||
metaJson: {
|
||||
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4',
|
||||
poster: 'https://images.unsplash.com/photo-1616401784845-180882ba9ba8.jpg',
|
||||
duration: '01:35',
|
||||
aspectRatio: '16:9',
|
||||
title: '跨国现代化智能立体仓全景实拍',
|
||||
},
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2019,
|
||||
cardId: 1005,
|
||||
type: 'case' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 3,
|
||||
title: '中日新能源车配件 48 小时达',
|
||||
content: '通过全链条条码及 RFID 联动,清关及通关提速 60%。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1578575437130-527eed3abbec.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2020,
|
||||
cardId: 1005,
|
||||
type: 'article' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 4,
|
||||
title: '区域全面经济伙伴关系协定 (RCEP) 关务实战指南',
|
||||
content: '深度解读原产地累积规则与跨国物流链条中的税费优化策略。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1542744173-8e7e53415bb0.jpg',
|
||||
status: 1,
|
||||
},
|
||||
],
|
||||
cases: [
|
||||
{
|
||||
id: 3005,
|
||||
cardId: 1005,
|
||||
title: '亚太区域核心零部件保税中转枢纽项目',
|
||||
client: 'Global AutoTech Consortium',
|
||||
duration: '2024.11 - 2025.07',
|
||||
metrics: '仓储周转效率提高 42%,破损率控制在 0.001% 以下',
|
||||
coverImage: 'https://images.unsplash.com/photo-1578575437130-527eed3abbec.jpg',
|
||||
description: '成功构建日韩到东盟的跨国零备件绿色走廊,达成准时化(JIT)精益交付。',
|
||||
},
|
||||
],
|
||||
articles: [
|
||||
{
|
||||
id: 4005,
|
||||
cardId: 1005,
|
||||
title: '全球地缘与贸易新常态下的弹性供应链构建',
|
||||
summary: '面对跨国运价波动与港口堵塞风险,如何通过数字孪生提升全球业务韧性。',
|
||||
content: '跨国供应链的制胜关键已经从单一的价格竞争转向全链路的可视化与动态避险能力...',
|
||||
coverImage: 'https://images.unsplash.com/photo-1542744173-8e7e53415bb0.jpg',
|
||||
isPublished: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 批量将 Mock 卡片数据写入/同步至底层数据库中 (幂等设计)
|
||||
*/
|
||||
export async function seedCardsToDatabase(dataSource: any): Promise<number> {
|
||||
if (!dataSource) return 0;
|
||||
|
||||
const cardRepo = dataSource.getRepository(CardInfoEntity);
|
||||
const bentoRepo = dataSource.getRepository(CardBentoEntity);
|
||||
const casesRepo = dataSource.getRepository(CardCasesEntity);
|
||||
const articleRepo = dataSource.getRepository(CardArticleEntity);
|
||||
|
||||
let insertedCount = 0;
|
||||
|
||||
for (const bundle of MOCK_CARDS_DATA) {
|
||||
// 1. 保存或更新名片主表
|
||||
let card = await cardRepo.findOne({ where: { slug: bundle.card.slug } });
|
||||
if (!card) {
|
||||
card = cardRepo.create(bundle.card);
|
||||
} else {
|
||||
Object.assign(card, bundle.card);
|
||||
}
|
||||
const savedCard = await cardRepo.save(card);
|
||||
insertedCount++;
|
||||
|
||||
const cardId = Number(savedCard.id);
|
||||
|
||||
// 2. 清理旧 Bento 块并批量写入新 Bento 块
|
||||
await bentoRepo.delete({ cardId });
|
||||
for (const b of bundle.bentoBlocks) {
|
||||
const bento = bentoRepo.create({
|
||||
...b,
|
||||
cardId,
|
||||
});
|
||||
await bentoRepo.save(bento);
|
||||
}
|
||||
|
||||
// 3. 写入关联案例
|
||||
await casesRepo.delete({ cardId });
|
||||
for (const c of bundle.cases) {
|
||||
const cEntity = casesRepo.create({
|
||||
...c,
|
||||
cardId,
|
||||
});
|
||||
await casesRepo.save(cEntity);
|
||||
}
|
||||
|
||||
// 4. 写入关联文章
|
||||
await articleRepo.delete({ cardId });
|
||||
for (const a of bundle.articles) {
|
||||
const aEntity = articleRepo.create({
|
||||
...a,
|
||||
cardId,
|
||||
});
|
||||
await articleRepo.save(aEntity);
|
||||
}
|
||||
}
|
||||
|
||||
return insertedCount;
|
||||
}
|
||||
@@ -83,7 +83,7 @@ export function validateLocaleFields(fields: CardLocaleFields): {
|
||||
}
|
||||
|
||||
/**
|
||||
* 出海团队多语言名片批量生成向导与多语种字段镜像校验服务
|
||||
* 国际团队多语言名片批量生成向导与多语种字段镜像校验服务
|
||||
*/
|
||||
@Provide()
|
||||
export class CardBatchLocaleService extends BaseService {
|
||||
|
||||
@@ -12,8 +12,8 @@ export interface GetCasesByCardIdParams {
|
||||
}
|
||||
|
||||
/**
|
||||
* 出海案例作品集服务
|
||||
* 提供出海案例的标准 CRUD 及按名片筛选能力
|
||||
* 标杆案例作品集服务
|
||||
* 提供标杆案例的标准 CRUD 及按名片筛选能力
|
||||
*/
|
||||
@Provide()
|
||||
export class CardCasesService extends BaseService {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BaseController, CoolController, CoolCommException } from '@cool-midway/
|
||||
import { ComplianceGdprService } from '../../service/compliance';
|
||||
|
||||
/**
|
||||
* 管理后台 - 海外数据隐私与合规治理 (GDPR / CCPA)
|
||||
* 管理后台 - 国际数据隐私与合规治理 (GDPR / CCPA)
|
||||
* 路由前缀: /admin/crm/compliance
|
||||
*/
|
||||
@Provide()
|
||||
|
||||
@@ -68,7 +68,7 @@ export function normalizeTrafficChannel(
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算全链路出海留资转化漏斗指标
|
||||
* 计算全链路留资转化漏斗指标
|
||||
*/
|
||||
export function calculateAttributionFunnel(
|
||||
events: ChannelAttributionEvent[]
|
||||
@@ -118,7 +118,7 @@ export function calculateAttributionFunnel(
|
||||
}
|
||||
|
||||
/**
|
||||
* 访客雷达多渠道归因与出海转化分析服务
|
||||
* 访客雷达多渠道归因与业务转化分析服务
|
||||
*/
|
||||
@Provide()
|
||||
export class RadarAttributionService extends BaseService {
|
||||
|
||||
@@ -68,7 +68,7 @@ export function anonymizePiiData(rawRecord: {
|
||||
}
|
||||
|
||||
/**
|
||||
* 海外合规 GDPR / CCPA 隐私治理服务
|
||||
* 国际合规 GDPR / CCPA 隐私治理服务
|
||||
*/
|
||||
@Provide()
|
||||
export class ComplianceGdprService extends BaseService {
|
||||
|
||||
@@ -116,7 +116,7 @@ export function enrichCompanyProfile(
|
||||
}
|
||||
|
||||
/**
|
||||
* 出海潜客企业邮箱域名解析与商业主页自动富化服务
|
||||
* 潜客企业邮箱域名解析与商业主页自动富化服务
|
||||
*/
|
||||
@Provide()
|
||||
export class LeadCompanyEnrichmentService extends BaseService {
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface GeoHeatmapReport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 全球常见出海枢纽城市经纬度字典 [经度 lng, 纬度 lat]
|
||||
* 全球常见商务枢纽城市经纬度字典 [经度 lng, 纬度 lat]
|
||||
*/
|
||||
const CITY_COORDINATES: Record<string, [number, number]> = {
|
||||
'AE:Dubai': [55.27, 25.20],
|
||||
@@ -53,7 +53,7 @@ const COUNTRY_FALLBACK_COORDINATES: Record<string, [number, number]> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析出海城市经纬度坐标
|
||||
* 解析国际城市经纬度坐标
|
||||
*/
|
||||
export function resolveCityCoordinates(
|
||||
country: string,
|
||||
|
||||
@@ -95,7 +95,7 @@ export function evaluateLeadStagePromotion(
|
||||
}
|
||||
|
||||
/**
|
||||
* 出海潜客线索自动化阶段流转服务
|
||||
* 潜客线索自动化阶段流转服务
|
||||
*/
|
||||
@Provide()
|
||||
export class LeadStageAutomationService extends BaseService {
|
||||
|
||||
@@ -211,10 +211,10 @@ export class CrmLeadScoreService extends BaseService {
|
||||
} else if (level === 'Warm') {
|
||||
suggestion = `该潜客(综合意向得分 ${score})对品牌与产品具有明确兴趣。建议在 24 小时内通过邮件或即时通讯推送针对性解决方案白皮书与客户案例,加深意向转化。`;
|
||||
} else {
|
||||
suggestion = `该潜客(综合意向得分 ${score})目前处于初次轻度探索阶段。建议纳入全球出海自动化培育邮件流 (Nurture Sequence),定期推送行业洞察,避免过早强行推销。`;
|
||||
suggestion = `该潜客(综合意向得分 ${score})目前处于初次轻度探索阶段。建议纳入全球自动化培育邮件流 (Nurture Sequence),定期推送行业洞察,避免过早强行推销。`;
|
||||
}
|
||||
|
||||
// 2. 出海商务破冰首语 (Icebreaker)
|
||||
// 2. 国际商务破冰首语 (Icebreaker)
|
||||
let icebreaker = '';
|
||||
const nameGreeting = name ? name : 'there';
|
||||
const companyAffix = company ? ` from ${company}` : '';
|
||||
@@ -246,7 +246,7 @@ export class CrmLeadScoreService extends BaseService {
|
||||
highlights.push('主动留存了真实联系渠道');
|
||||
}
|
||||
if (country && country !== 'Unknown' && country !== 'Local') {
|
||||
highlights.push(`来自 ${country}${city ? ' · ' + city : ''} 的出海潜在客商`);
|
||||
highlights.push(`来自 ${country}${city ? ' · ' + city : ''} 的潜在商业客户`);
|
||||
}
|
||||
if (highlights.length === 0) {
|
||||
highlights.push('初次访问数字名片微官网');
|
||||
|
||||
@@ -16,18 +16,28 @@ import {
|
||||
} from '../../../comm/types/omni';
|
||||
import { lookupGeoIp } from '../../../comm/geoip/lookup';
|
||||
|
||||
interface WorkplaceTrackInput {
|
||||
export interface WorkplaceTrackInput {
|
||||
id?: number | string;
|
||||
cardId?: number | string;
|
||||
visitorId?: string;
|
||||
ip?: string;
|
||||
country?: string;
|
||||
city?: string;
|
||||
touchType?: TouchType;
|
||||
stayDurationMs?: number;
|
||||
scrollDepth?: number;
|
||||
viewedBlocks?: string[];
|
||||
createTime?: Date | string;
|
||||
createdAt?: Date | string;
|
||||
}
|
||||
|
||||
export interface WorkplaceCardMeta {
|
||||
id: number | string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
company?: string;
|
||||
}
|
||||
|
||||
interface WorkplaceLeadInput {
|
||||
status?: LeadStatus;
|
||||
createTime?: Date | string;
|
||||
@@ -51,13 +61,57 @@ function resolveEventTime(item: { createTime?: Date | string; createdAt?: Date |
|
||||
}
|
||||
|
||||
/**
|
||||
* 纯函数:按今日窗口聚合工作台 KPI 与雷达动态
|
||||
* 纯函数:多因子商务意向度科学评分模型 (0 - 99 分)
|
||||
* 权重:渠道价值基准 (35%) + 停留时长阶梯 (40%) + 互动/滚动深度 (25%)
|
||||
*/
|
||||
export function calculateTrackIntentScore(track: {
|
||||
touchType?: string;
|
||||
stayDurationMs?: number;
|
||||
scrollDepth?: number;
|
||||
viewedBlocks?: string[];
|
||||
}): number {
|
||||
// 1. 渠道基准分
|
||||
let baseScore = 10;
|
||||
if (track.touchType === 'nfc') {
|
||||
baseScore = 35; // 面对面触碰交换
|
||||
} else if (track.touchType === 'qr') {
|
||||
baseScore = 25; // 展会物料自主扫码
|
||||
}
|
||||
|
||||
// 2. 停留时长阶梯 (秒)
|
||||
const staySec = Math.round((track.stayDurationMs || 0) / 1000);
|
||||
let timeScore = 0;
|
||||
if (staySec <= 3) {
|
||||
timeScore = 0; // 0-3秒快速跳出 (Bounce),不予加分
|
||||
} else if (staySec <= 15) {
|
||||
timeScore = 10; // 浅层瞥视
|
||||
} else if (staySec <= 45) {
|
||||
timeScore = 20; // 正常阅读
|
||||
} else if (staySec <= 120) {
|
||||
timeScore = 30; // 深度了解
|
||||
} else {
|
||||
timeScore = 40; // 极高研读兴趣
|
||||
}
|
||||
|
||||
// 3. 互动与浏览深度
|
||||
const blocksCount = Array.isArray(track.viewedBlocks) ? track.viewedBlocks.length : 0;
|
||||
const blockScore = Math.min(blocksCount * 5, 20);
|
||||
|
||||
const scrollScore = track.scrollDepth && track.scrollDepth >= 60 ? 10 : 0;
|
||||
|
||||
const total = baseScore + timeScore + blockScore + scrollScore;
|
||||
return Math.max(5, Math.min(99, total));
|
||||
}
|
||||
|
||||
/**
|
||||
* 纯函数:按今日窗口聚合工作台 KPI 与雷达动态 (支持会话去噪与名片关联)
|
||||
*/
|
||||
export function buildWorkplaceOverview(
|
||||
tracks: WorkplaceTrackInput[],
|
||||
leads: WorkplaceLeadInput[],
|
||||
now: Date,
|
||||
feedLimit = 12
|
||||
feedLimit = 12,
|
||||
cards: WorkplaceCardMeta[] = []
|
||||
): WorkplaceOverviewResult {
|
||||
const todayStart = startOfLocalDay(now).getTime();
|
||||
const yesterdayStart = todayStart - 24 * 60 * 60 * 1000;
|
||||
@@ -107,21 +161,92 @@ export function buildWorkplaceOverview(
|
||||
}
|
||||
}
|
||||
|
||||
const radarFeed: WorkplaceRadarItem[] = [...tracks]
|
||||
// 名片标题映射字典
|
||||
const cardTitleMap = new Map<string, string>();
|
||||
for (const c of cards) {
|
||||
const parts = [c.name, c.title].filter(Boolean);
|
||||
const label = parts.length > 0 ? parts.join(' · ') : (c.company || `名片 #${c.id}`);
|
||||
cardTitleMap.set(String(c.id), label);
|
||||
}
|
||||
|
||||
// 会话降噪聚合:针对相同 visitorId + cardId 合并初次触碰与离开结算
|
||||
const sessionMap = new Map<string, WorkplaceTrackInput>();
|
||||
const unkeyedTracks: WorkplaceTrackInput[] = [];
|
||||
|
||||
for (const t of tracks) {
|
||||
if (t.visitorId && t.cardId) {
|
||||
const key = `${t.visitorId}_${t.cardId}`;
|
||||
const prev = sessionMap.get(key);
|
||||
if (!prev) {
|
||||
sessionMap.set(key, { ...t });
|
||||
} else {
|
||||
// 合并停留时长 (取较大值)
|
||||
prev.stayDurationMs = Math.max(prev.stayDurationMs || 0, t.stayDurationMs || 0);
|
||||
// 合并滚动深度 (取较大值)
|
||||
prev.scrollDepth = Math.max(prev.scrollDepth || 0, t.scrollDepth || 0);
|
||||
// 合并浏览组件
|
||||
const prevBlocks = Array.isArray(prev.viewedBlocks) ? prev.viewedBlocks : [];
|
||||
const nextBlocks = Array.isArray(t.viewedBlocks) ? t.viewedBlocks : [];
|
||||
prev.viewedBlocks = Array.from(new Set([...prevBlocks, ...nextBlocks]));
|
||||
// 时间取最新
|
||||
if (resolveEventTime(t) > resolveEventTime(prev)) {
|
||||
prev.createTime = t.createTime ?? t.createdAt;
|
||||
prev.id = t.id ?? prev.id;
|
||||
}
|
||||
// 触碰渠道优先级: nfc > qr > link
|
||||
if (t.touchType === 'nfc') {
|
||||
prev.touchType = 'nfc';
|
||||
} else if (t.touchType === 'qr' && prev.touchType !== 'nfc') {
|
||||
prev.touchType = 'qr';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unkeyedTracks.push(t);
|
||||
}
|
||||
}
|
||||
|
||||
const consolidatedTracks = [...sessionMap.values(), ...unkeyedTracks];
|
||||
|
||||
const radarFeed: WorkplaceRadarItem[] = consolidatedTracks
|
||||
.sort((a, b) => resolveEventTime(b) - resolveEventTime(a))
|
||||
.slice(0, take)
|
||||
.map((item) => ({
|
||||
id: item.id ?? 0,
|
||||
cardId: item.cardId ?? 0,
|
||||
country: item.country || 'Unknown',
|
||||
city: item.city || 'Unknown',
|
||||
touchType: item.touchType && ['nfc', 'qr', 'link'].includes(item.touchType)
|
||||
? item.touchType
|
||||
: 'link',
|
||||
stayDurationMs: Math.max(0, Math.round(item.stayDurationMs || 0)),
|
||||
viewedBlocks: Array.isArray(item.viewedBlocks) ? item.viewedBlocks.map(String) : [],
|
||||
createTime: item.createTime ?? item.createdAt ?? now,
|
||||
}));
|
||||
.map((item) => {
|
||||
const isLocal =
|
||||
item.country === 'Local' ||
|
||||
item.city === 'Local' ||
|
||||
(Boolean(item.ip) &&
|
||||
(item.ip === '127.0.0.1' ||
|
||||
item.ip === '::1' ||
|
||||
item.ip?.startsWith('192.168.') ||
|
||||
item.ip?.startsWith('10.') ||
|
||||
item.ip?.toLowerCase() === 'localhost'));
|
||||
|
||||
const intentScore = calculateTrackIntentScore({
|
||||
touchType: item.touchType,
|
||||
stayDurationMs: item.stayDurationMs,
|
||||
scrollDepth: item.scrollDepth,
|
||||
viewedBlocks: item.viewedBlocks,
|
||||
});
|
||||
|
||||
const cardTitle = item.cardId ? cardTitleMap.get(String(item.cardId)) : undefined;
|
||||
|
||||
return {
|
||||
id: item.id ?? 0,
|
||||
cardId: item.cardId ?? 0,
|
||||
cardTitle,
|
||||
visitorId: item.visitorId,
|
||||
country: item.country || 'Unknown',
|
||||
city: item.city || 'Unknown',
|
||||
touchType: item.touchType && ['nfc', 'qr', 'link'].includes(item.touchType)
|
||||
? item.touchType
|
||||
: 'link',
|
||||
stayDurationMs: Math.max(0, Math.round(item.stayDurationMs || 0)),
|
||||
viewedBlocks: Array.isArray(item.viewedBlocks) ? item.viewedBlocks.map(String) : [],
|
||||
intentScore,
|
||||
isLocal: Boolean(isLocal),
|
||||
createTime: item.createTime ?? item.createdAt ?? now,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
kpis: {
|
||||
@@ -238,7 +363,7 @@ export class CrmRadarService extends BaseService {
|
||||
): IpLocationResult {
|
||||
const rawHeaders = headers || (this.ctx && this.ctx.headers) || {};
|
||||
|
||||
// 1. 优先提取出海 CDN (如 Cloudflare) 携带的高可靠地理位置信息
|
||||
// 1. 优先提取全球 CDN (如 Cloudflare) 携带的高可靠地理位置信息
|
||||
const cdnCountry =
|
||||
rawHeaders['cf-ipcountry'] ||
|
||||
rawHeaders['x-country'] ||
|
||||
@@ -651,13 +776,24 @@ export class CrmRadarService extends BaseService {
|
||||
feedLimit?: number;
|
||||
}): Promise<WorkplaceOverviewResult> {
|
||||
const feedLimit = params?.feedLimit;
|
||||
const [tracks, leads] = await Promise.all([
|
||||
const [tracks, leads, cards] = await Promise.all([
|
||||
this.crmTrackEntity.find({
|
||||
order: { id: 'DESC' },
|
||||
take: 500,
|
||||
}),
|
||||
this.crmLeadEntity.find(),
|
||||
this.cardInfoEntity
|
||||
? this.cardInfoEntity.find({
|
||||
select: ['id', 'name', 'title', 'company'],
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
return buildWorkplaceOverview(tracks, leads, new Date(), feedLimit);
|
||||
return buildWorkplaceOverview(
|
||||
tracks,
|
||||
leads,
|
||||
new Date(),
|
||||
feedLimit,
|
||||
cards as WorkplaceCardMeta[]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export type HttpPosterFn = (
|
||||
) => Promise<{ statusCode: number; data?: any }>;
|
||||
|
||||
/**
|
||||
* 出海商机实时 Webhook 触发器与签名分发服务
|
||||
* 全球商机实时 Webhook 触发器与签名分发服务
|
||||
*/
|
||||
@Provide()
|
||||
export class LeadWebhookDispatcher extends BaseService {
|
||||
|
||||
@@ -98,7 +98,7 @@ export function calculateBatchYieldRate(
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨境实体卡供应链生产批次工单管理服务
|
||||
* 国际实体卡供应链生产批次工单管理服务
|
||||
*/
|
||||
@Provide()
|
||||
export class NfcManufacturingService extends BaseService {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BaseService } from '@cool-midway/core';
|
||||
import { SupportedCurrency, TaxCalculationResult } from '../../../comm/types/omni';
|
||||
|
||||
/**
|
||||
* 相对 USD 的基准出海商业结算汇率表
|
||||
* 相对 USD 的基准全球商业结算汇率表
|
||||
*/
|
||||
const USD_RATES: Record<SupportedCurrency, number> = {
|
||||
USD: 1.0,
|
||||
@@ -75,7 +75,7 @@ export function convertCurrency(
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算国际出海合规税率与税额
|
||||
* 计算国际商业合规税率与税额
|
||||
*/
|
||||
export function calculateInternationalTax(
|
||||
params: TaxCalculationParams
|
||||
@@ -130,7 +130,7 @@ export function calculateInternationalTax(
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨境多币种汇率换算与出海发票税额计算服务
|
||||
* 国际多币种汇率换算与合规发票税额计算服务
|
||||
*/
|
||||
@Provide()
|
||||
export class CurrencyTaxCalculatorService extends BaseService {
|
||||
|
||||
@@ -82,7 +82,7 @@ export function assertLeadAccessPermission(
|
||||
}
|
||||
|
||||
/**
|
||||
* 出海多租户团队成员细粒度数据隔离服务
|
||||
* 多租户团队成员细粒度数据隔离服务
|
||||
*/
|
||||
@Provide()
|
||||
export class TeamDataScopeService extends BaseService {
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from '../../../comm/types/omni';
|
||||
|
||||
/**
|
||||
* 商业化新账与出海财务运营数据分析服务
|
||||
* 商业化新账与全球财务运营数据分析服务
|
||||
*/
|
||||
@Provide()
|
||||
export class TeamFinanceAnalyticsService extends BaseService {
|
||||
@@ -107,7 +107,7 @@ export class TeamFinanceAnalyticsService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 兜底数据与真实数据融合(确保未注入数据库或冷启动时拥有出海基准值)
|
||||
// 5. 兜底数据与真实数据融合(确保未注入数据库或冷启动时拥有行业基准值)
|
||||
if (activeInvoices.length === 0) {
|
||||
const periodMultipliers: Record<FinanceAnalyticsPeriod, number> = {
|
||||
today: 1,
|
||||
@@ -252,7 +252,7 @@ export class TeamFinanceAnalyticsService extends BaseService {
|
||||
},
|
||||
{
|
||||
plan: 'pro',
|
||||
name: 'Pro 专业出海版',
|
||||
name: 'Pro 专业商务版',
|
||||
accountCount: activeProCount,
|
||||
mrrContribution: activeProCount * proMonthlyPrice,
|
||||
seatUtilization: 82,
|
||||
@@ -266,7 +266,7 @@ export class TeamFinanceAnalyticsService extends BaseService {
|
||||
},
|
||||
];
|
||||
|
||||
// 10. 全球出海区域营收贡献 (COO)
|
||||
// 10. 全球区域营收贡献 (COO)
|
||||
const regionalRevenue: RegionalRevenueItem[] = [
|
||||
{
|
||||
region: '北美市场 (US / CA)',
|
||||
|
||||
@@ -32,7 +32,7 @@ export class PlatformSubscriptionService extends BaseService {
|
||||
{
|
||||
id: 1,
|
||||
tenantId: 1,
|
||||
tenantName: 'Vanguard Global Trade Ltd. (出海先锋外贸)',
|
||||
tenantName: 'Vanguard Global Trade Ltd. (全球商业先锋)',
|
||||
adminName: 'David Zhang',
|
||||
adminEmail: 'david@vanguard-trade.com',
|
||||
adminPhone: '+86 13800138001',
|
||||
@@ -224,7 +224,7 @@ export class PlatformSubscriptionService extends BaseService {
|
||||
{
|
||||
planType: 'free',
|
||||
name: 'Free 基础体验版',
|
||||
description: '适合个人自由职业者与初创独立人,轻量体验出海名片微官网',
|
||||
description: '适合个人自由职业者与初创独立人,轻量体验智能名片微官网',
|
||||
monthlyPrice: 0,
|
||||
yearlyPrice: 0,
|
||||
cardsLimit: 1,
|
||||
@@ -238,7 +238,7 @@ export class PlatformSubscriptionService extends BaseService {
|
||||
{
|
||||
planType: 'pro',
|
||||
name: 'Pro 专业商务版',
|
||||
description: '面向出海业务精英、外贸销售主管与跨国顾问的高性能全功能套件',
|
||||
description: '面向全球业务精英、销售主管与跨国顾问的高性能全功能套件',
|
||||
monthlyPrice: 49,
|
||||
yearlyPrice: 490,
|
||||
cardsLimit: 10,
|
||||
@@ -252,7 +252,7 @@ export class PlatformSubscriptionService extends BaseService {
|
||||
{
|
||||
planType: 'enterprise',
|
||||
name: 'Enterprise 商业企业版',
|
||||
description: '为出海跨国集团、参展销售天团定制的多租户公海池与品牌统一管控',
|
||||
description: '为跨国集团与全球销售团队定制的多租户公海池与品牌统一管控',
|
||||
monthlyPrice: 299,
|
||||
yearlyPrice: 2990,
|
||||
cardsLimit: 100,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
MeetingEventOptions,
|
||||
} from '../../src/modules/card/service/calendar';
|
||||
|
||||
describe('Point 42: 出海潜客跨国会议 Google Calendar / Outlook 一键预约同步', () => {
|
||||
describe('Point 42: 跨国会议与潜客预约 Google Calendar / Outlook 一键同步', () => {
|
||||
let calendarService: CardCalendarService;
|
||||
|
||||
const mockMeeting: MeetingEventOptions = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ContactWindowInfo,
|
||||
} from '../../src/comm/i18n/geo';
|
||||
|
||||
describe('Point 39: 出海潜客多时区识别与最佳联系时间窗口推荐 (Best Contact Time Window)', () => {
|
||||
describe('Point 39: 潜客多时区识别与最佳联系时间窗口推荐 (Best Contact Time Window)', () => {
|
||||
describe('1. 国际多国家/时区时间计算', () => {
|
||||
it('应准确计算中东阿联酋 (UTC+4) 当地时间与联系窗口', () => {
|
||||
// 假设 UTC 时间为 06:00 (北京时间 14:00,阿联酋当地时间为 10:00)
|
||||
@@ -20,7 +20,7 @@ describe('Point 39: 出海潜客多时区识别与最佳联系时间窗口推荐
|
||||
expect(windowInfo.suggestionText).toContain('当地工作时间');
|
||||
});
|
||||
|
||||
it('当海外客商处于深夜休息时段 (如欧洲当地 23:00) 时,建议勿扰', () => {
|
||||
it('当国际客商处于深夜休息时段 (如欧洲当地 23:00) 时,建议勿扰', () => {
|
||||
// 假设 UTC 时间为 22:00 (欧洲 CET UTC+1 当地为 23:00)
|
||||
const baseUtcDate = new Date(Date.UTC(2026, 8, 21, 22, 0, 0));
|
||||
|
||||
|
||||
@@ -406,7 +406,7 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
audioPronunciationUrl: 'https://cdn.omnicard.ai/audio/alex-pronunciation.mp3',
|
||||
status: 'published',
|
||||
themeCode: 'corporate_navy',
|
||||
remark: '内部高管名片,核心出海带队',
|
||||
remark: '内部高管名片,国际业务核心',
|
||||
internalConfig: '{"rateLimit": 1000, "debug": false}',
|
||||
});
|
||||
|
||||
@@ -425,7 +425,7 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
title: '个人简介与身份卡',
|
||||
content: '专注于中东与欧洲出海数字商务拓客',
|
||||
content: '专注于中东与欧洲数字商务合作',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
@@ -440,7 +440,7 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
type: 'case' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 2 as BentoRowSpan,
|
||||
title: '新能源出海案例',
|
||||
title: '新能源国际合作案例',
|
||||
content: '协助头部光伏企业落地阿布扎比',
|
||||
status: 1,
|
||||
},
|
||||
@@ -448,8 +448,8 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
type: 'article' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
title: '2026出海白皮书',
|
||||
content: '深度解读跨境智能名片获客漏斗',
|
||||
title: '2026全球数字名片白皮书',
|
||||
content: '深度解读全球智能名片获客漏斗',
|
||||
status: 1,
|
||||
},
|
||||
];
|
||||
@@ -488,14 +488,14 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
it('P04: 博客文章发布与 SEO OpenGraph 社交卡片自动提炼', async () => {
|
||||
const rawHtmlContent = `
|
||||
<article>
|
||||
<h1>出海获客新引擎:2026 跨境数字名片实战洞察</h1>
|
||||
<p>在全球数字化出海与国际展会场景中,传统的纸质名片不仅浪费印刷成本,且存在高达 88% 的即时丢弃率。通过引入具备 NFC 极速碰触、多语言自适应以及 24 小时 AI 自动留资的智能微官网,出海销售代表能够在 30 秒内建立品牌信任,并让每一位海外客商的浏览轨迹实时映射在销售雷达上,实现端到端的精准线索转化。</p>
|
||||
<h1>全球获客新引擎:2026 数字名片实战洞察</h1>
|
||||
<p>在全球数字化与国际商务展会场景中,传统的纸质名片不仅浪费印刷成本,且存在高达 88% 的即时丢弃率。通过引入具备 NFC 极速碰触、多语言自适应以及 24 小时 AI 自动留资的智能微官网,销售代表能够在 30 秒内建立品牌信任,并让每一位国际客商的浏览轨迹实时映射在销售雷达上,实现端到端的精准线索转化。</p>
|
||||
</article>
|
||||
`;
|
||||
|
||||
const articleEntity = await articleRepo.save({
|
||||
cardId: sharedCardId,
|
||||
title: '出海获客新引擎:2026 跨境数字名片实战洞察',
|
||||
title: '全球获客新引擎:2026 数字名片实战洞察',
|
||||
content: rawHtmlContent,
|
||||
coverImage: 'https://cdn.omnicard.ai/articles/og-banner.png',
|
||||
isPublished: true,
|
||||
@@ -506,7 +506,7 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
|
||||
expect(ogMeta.title).toBe(articleEntity.title);
|
||||
expect(ogMeta.image).toBe('https://cdn.omnicard.ai/articles/og-banner.png');
|
||||
expect(ogMeta.description).toContain('在全球数字化出海与国际展会场景中');
|
||||
expect(ogMeta.description).toContain('在全球数字化与国际商务展会场景中');
|
||||
expect(ogMeta.description.length).toBeLessThanOrEqual(160);
|
||||
expect(ogMeta.description).not.toContain('<p>');
|
||||
});
|
||||
@@ -746,7 +746,7 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
it('P13: 名片 AI 顾问人设、基调与留资触发阈值配置', async () => {
|
||||
const config = await aiConciergeService.saveByCardId(sharedCardId, {
|
||||
personaType: 'trade_expert',
|
||||
welcomeMsg: '您好!我是 Alex 的 AI 出海顾问,请问有什么可以协助您的?',
|
||||
welcomeMsg: '您好!我是 Alex 的 AI 商务顾问,请问有什么可以协助您的?',
|
||||
triggerLeadThreshold: 75,
|
||||
promptTemplate: 'You are an export trade consultant specialized in clean energy solutions.',
|
||||
toneStyle: 'professional',
|
||||
@@ -758,14 +758,14 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
expect(config.triggerLeadThreshold).toBe(75);
|
||||
|
||||
const queried = await aiConciergeService.getByCardId(sharedCardId);
|
||||
expect(queried?.welcomeMsg).toContain('Alex 的 AI 出海顾问');
|
||||
expect(queried?.welcomeMsg).toContain('Alex 的 AI 商务顾问');
|
||||
});
|
||||
|
||||
it('P14: 知识库 RAG 滑动窗口文本分块与重叠切片算法', () => {
|
||||
const rawDocText = `
|
||||
OmniCard AI 致力于为全球商务出海团队提供次世代数字名片与微官网解决方案。
|
||||
OmniCard AI 致力于为全球商务团队提供次世代数字名片与微官网解决方案。
|
||||
支持中东、欧洲及北美地区的主流商务支付与 CRM 系统无缝互联。
|
||||
我们针对新能源、智能硬件及跨境电商客户提供定制化的展会获客方案,
|
||||
我们针对新能源、智能硬件及全球电商客户提供定制化的展会获客方案,
|
||||
配合实体 NFC 智能卡片,可实现秒级留资并自动将高意向线索同步至公海池。
|
||||
`.trim();
|
||||
|
||||
@@ -782,7 +782,7 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
const testChunks = [
|
||||
{
|
||||
index: 0,
|
||||
content: '公司主营产品包含新能源储能电站与光伏逆变器海外解决方案。',
|
||||
content: '公司主营产品包含新能源储能电站与光伏逆变器全球解决方案。',
|
||||
length: 30,
|
||||
},
|
||||
{
|
||||
@@ -808,7 +808,7 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
expect(results[0].score).toBeGreaterThan(0.5);
|
||||
});
|
||||
|
||||
it('P16: 外贸行业术语库 (Glossary) 导入与出海多语言润色替换', async () => {
|
||||
it('P16: 行业专有术语库 (Glossary) 导入与国际多语言润色替换', async () => {
|
||||
const importRes = await aiGlossaryService.batchImport([
|
||||
{
|
||||
sourceTerm: 'digital business card',
|
||||
@@ -1103,15 +1103,15 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
|
||||
// =========================================================================
|
||||
describe('主干流 6: 多租户与商业化 (Points 27-30)', () => {
|
||||
it('P27: 组织架构部门树多层级递归与成员名片分配 (TeamMembersService)', async () => {
|
||||
// 1. 初始化部门树结构:总部 -> 海外销售中心 -> 中东拓展组
|
||||
// 1. 初始化部门树结构:总部 -> 国际销售中心 -> 中东拓展组
|
||||
const headDept = await deptRepo.save({
|
||||
name: '全球出海总部',
|
||||
name: '全球总部',
|
||||
parentId: null as any,
|
||||
orderNum: 1,
|
||||
});
|
||||
|
||||
const salesDept = await deptRepo.save({
|
||||
name: '海外拓展事业群',
|
||||
name: '全球拓展事业群',
|
||||
parentId: headDept.id,
|
||||
orderNum: 2,
|
||||
});
|
||||
|
||||
@@ -54,13 +54,13 @@ describe('T31 DeepSeek 大模型集成与适配服务 (deepseek.ts)', () => {
|
||||
it('2.1 buildSystemPrompt 组装名片主、人设与知识库切片', () => {
|
||||
const card = {
|
||||
name: '张总',
|
||||
company: '华强出海科技',
|
||||
title: '海外商务总监',
|
||||
company: '华强全球科技',
|
||||
title: '国际商务总监',
|
||||
};
|
||||
const concierge = {
|
||||
personaType: 'trade_expert' as const,
|
||||
toneStyle: 'professional' as const,
|
||||
promptTemplate: '你是一名资深出海贸易专家,精通中东与东南亚市场。',
|
||||
promptTemplate: '你是一名资深国际贸易专家,精通中东与东南亚市场。',
|
||||
};
|
||||
const chunks = [
|
||||
{ index: 1, content: '支持信用证结算与 FOB 迪拜港条款。' },
|
||||
@@ -70,9 +70,9 @@ describe('T31 DeepSeek 大模型集成与适配服务 (deepseek.ts)', () => {
|
||||
const systemPrompt = service.buildSystemPrompt(card, concierge, chunks);
|
||||
|
||||
expect(systemPrompt).toContain('张总');
|
||||
expect(systemPrompt).toContain('华强出海科技');
|
||||
expect(systemPrompt).toContain('海外商务总监');
|
||||
expect(systemPrompt).toContain('你是一名资深出海贸易专家');
|
||||
expect(systemPrompt).toContain('华强全球科技');
|
||||
expect(systemPrompt).toContain('国际商务总监');
|
||||
expect(systemPrompt).toContain('你是一名资深国际贸易专家');
|
||||
expect(systemPrompt).toContain('支持信用证结算与 FOB 迪拜港条款。');
|
||||
expect(systemPrompt).toContain('最小起订量 (MOQ) 为 500 台。');
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ function localizeReq(
|
||||
};
|
||||
}
|
||||
|
||||
describe('T39: AI 出海润色 (modules/ai localize)', () => {
|
||||
describe('T39: AI 多语言本地化润色 (modules/ai localize)', () => {
|
||||
describe('1. AiLocalizeService.localize 入参校验', () => {
|
||||
let service: AiLocalizeService;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('Point 47: AI 销售潜客智能跟进邮件/消息地道草拟引擎',
|
||||
service = new AiOutreachDraftService();
|
||||
});
|
||||
|
||||
describe('1. formatOutreachPrompt (出海商务邮件地道提示词组装)', () => {
|
||||
describe('1. formatOutreachPrompt (全球商务邮件地道提示词组装)', () => {
|
||||
it('英语场景应包含地道商务开场、兴趣针对性提及与日程预约呼吁', () => {
|
||||
const req: OutreachDraftRequest = {
|
||||
leadName: 'Arthur Pendelton',
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('Point 07: AI 知识库分块切片与召回沙盒 (modules/ai knowledg
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
content: '雷达系统支持出海 CRM 访客轨迹追踪、停留时长与意向度自动评分。',
|
||||
content: '雷达系统支持智能 CRM 访客轨迹追踪、停留时长与意向度自动评分。',
|
||||
length: 33,
|
||||
},
|
||||
{
|
||||
@@ -150,10 +150,10 @@ describe('Point 07: AI 知识库分块切片与召回沙盒 (modules/ai knowledg
|
||||
|
||||
it('应兼容接收纯字符串数组 chunks', () => {
|
||||
const stringChunks = [
|
||||
'出海多语言支持英文、中文、阿拉伯语',
|
||||
'国际多语言支持英文、中文、阿拉伯语',
|
||||
'国内网络环境配置说明',
|
||||
];
|
||||
const results = service.simulateRetrieval('阿拉伯语 出海', stringChunks, 2);
|
||||
const results = service.simulateRetrieval('阿拉伯语 国际多语言', stringChunks, 2);
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].content).toContain('阿拉伯语');
|
||||
expect(results[0].score).toBeGreaterThan(0);
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('Point 61 (P1): AI 对话客商意图与情绪极性分析引擎', () =
|
||||
analyzer = new AiSentimentIntentAnalyzer();
|
||||
});
|
||||
|
||||
describe('1. analyzeConversationIntent (出海商务意图识别)', () => {
|
||||
describe('1. analyzeConversationIntent (商务意图识别)', () => {
|
||||
it('包含价格、报价、起订量或购买关键词应识别为 PURCHASE_INTENT', () => {
|
||||
const msgs = [
|
||||
'Hello, what is the MOQ for 500 NFC metal cards?',
|
||||
|
||||
@@ -190,7 +190,7 @@ function seedBlocks(): Array<Partial<CardBentoEntity>> {
|
||||
rowSpan: 1,
|
||||
sortOrder: 1,
|
||||
title: '个人简介与身份卡',
|
||||
content: '全栈出海专家',
|
||||
content: '全球商务专家',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
@@ -211,7 +211,7 @@ function seedBlocks(): Array<Partial<CardBentoEntity>> {
|
||||
colSpan: 2,
|
||||
rowSpan: 2,
|
||||
sortOrder: 2,
|
||||
title: '新能源出海案例',
|
||||
title: '新能源国际合作案例',
|
||||
content: '中东客户成功交付',
|
||||
status: 0,
|
||||
},
|
||||
@@ -334,7 +334,7 @@ describe('后台 Bento 画布 CRUD (Admin Card Bento CoolController)', () => {
|
||||
type: 'video' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 2 as BentoRowSpan,
|
||||
title: '企业出海宣传片',
|
||||
title: '企业宣传片',
|
||||
mediaUrl: 'https://cdn.omnicard.ai/video/intro.mp4',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('后台博客文章与动态发布中心 (Card Article CoolController &
|
||||
it('完整文章入参时,应正确映射 title、summary 与 coverImage', () => {
|
||||
const article = {
|
||||
cardId: 101,
|
||||
title: '出海战略新视界:2026 跨境数字名片白皮书',
|
||||
title: '全球商务新视界:2026 数字名片白皮书',
|
||||
summary: '本文深度剖析全球化商务场景下 NFC 与微官网结合的数字化获客新路径。',
|
||||
content: '<p>正文内容...</p>',
|
||||
coverImage: 'https://cdn.omnicard.ai/articles/cover-1.jpg',
|
||||
@@ -91,18 +91,18 @@ describe('后台博客文章与动态发布中心 (Card Article CoolController &
|
||||
const longHtml = `
|
||||
<article>
|
||||
<h1>一级标题</h1>
|
||||
<p>在全球数字化浪潮席卷而来的今天,<strong>智能名片</strong>已经不仅是身份凭证,更是出海企业的第一道数字门面与线索转化漏斗。通过多语言自适应与 NFC 瞬时触达,销售代表可以在海外展会现场以极低摩擦完成商机留资与精准雷达跟进,大幅提升商机转化效率与客户体验。</p>
|
||||
<p>在全球数字化浪潮席卷而来的今天,<strong>智能名片</strong>已经不仅是身份凭证,更是现代企业的第一道数字门面与线索转化漏斗。通过多语言自适应与 NFC 瞬时触达,销售代表可以在国际展会现场以极低摩擦完成商机留资与精准雷达跟进,大幅提升商机转化效率与客户体验。</p>
|
||||
</article>
|
||||
`;
|
||||
const article = {
|
||||
cardId: 101,
|
||||
title: '出海营销指南',
|
||||
title: '全球营销指南',
|
||||
content: longHtml,
|
||||
isPublished: true,
|
||||
};
|
||||
|
||||
const og = service.generateOgMeta(article);
|
||||
expect(og.title).toBe('出海营销指南');
|
||||
expect(og.title).toBe('全球营销指南');
|
||||
expect(og.description.length).toBeLessThanOrEqual(160);
|
||||
expect(og.description).not.toContain('<p>');
|
||||
expect(og.description).not.toContain('<strong>');
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from '../../../src/modules/card/service/batch-locale';
|
||||
import { CardLocaleEntity } from '../../../src/modules/card/entity/locale';
|
||||
|
||||
describe('Point 43: 出海团队多语言名片批量生成向导与多语种字段镜像校验', () => {
|
||||
describe('Point 43: 跨国团队多语言名片批量生成向导与多语种字段镜像校验', () => {
|
||||
let service: CardBatchLocaleService;
|
||||
let mockRepository: any;
|
||||
let savedRecords: CardLocaleEntity[];
|
||||
@@ -80,8 +80,8 @@ describe('Point 43: 出海团队多语言名片批量生成向导与多语种字
|
||||
cardId: 101,
|
||||
baseFields: {
|
||||
name: '张三',
|
||||
title: '出海销售总监',
|
||||
company: '深圳数字智能出海科技',
|
||||
title: '国际业务总监',
|
||||
company: '数字智能科技',
|
||||
slogan: '智能连接全球',
|
||||
},
|
||||
},
|
||||
@@ -90,7 +90,7 @@ describe('Point 43: 出海团队多语言名片批量生成向导与多语种字
|
||||
baseFields: {
|
||||
name: '李四',
|
||||
title: '中东区域代表',
|
||||
company: '深圳数字智能出海科技',
|
||||
company: '数字智能科技',
|
||||
slogan: '专注海湾六国',
|
||||
},
|
||||
},
|
||||
@@ -164,7 +164,7 @@ describe('Point 43: 出海团队多语言名片批量生成向导与多语种字
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. inspectTeamLocaleCoverage (出海团队语言覆盖率与就绪度诊断)', () => {
|
||||
describe('3. inspectTeamLocaleCoverage (跨国团队语言覆盖率与就绪度诊断)', () => {
|
||||
it('应精准统计团队名片各语言的覆盖率与缺失名单', async () => {
|
||||
savedRecords.push(
|
||||
{ id: 1, cardId: 101, locale: 'en', name: 'Alex' } as CardLocaleEntity,
|
||||
|
||||
@@ -153,7 +153,7 @@ describe('后端微官网模块 (modules/card) - Bento Grid 便当盒组件树
|
||||
rowSpan: 1,
|
||||
sortOrder: 1,
|
||||
title: '个人简介与身份卡',
|
||||
content: '全栈出海专家',
|
||||
content: '全球商务专家',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
@@ -174,7 +174,7 @@ describe('后端微官网模块 (modules/card) - Bento Grid 便当盒组件树
|
||||
colSpan: 2,
|
||||
rowSpan: 2,
|
||||
sortOrder: 2,
|
||||
title: '新能源出海案例',
|
||||
title: '新能源国际合作案例',
|
||||
content: '中东客户成功交付',
|
||||
status: 0, // 禁用
|
||||
},
|
||||
@@ -275,7 +275,7 @@ describe('后端微官网模块 (modules/card) - Bento Grid 便当盒组件树
|
||||
type: 'video' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 2 as BentoRowSpan,
|
||||
title: '企业出海宣传片',
|
||||
title: '企业宣传片',
|
||||
mediaUrl: 'https://cdn.omnicard.ai/video/intro.mp4',
|
||||
},
|
||||
];
|
||||
@@ -336,7 +336,7 @@ describe('后端微官网模块 (modules/card) - Bento Grid 便当盒组件树
|
||||
type: 'form' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
title: '外贸留资表单',
|
||||
title: '商务留资表单',
|
||||
metaJson: JSON.stringify({
|
||||
fields: ['name', 'email', 'country', 'requirement'],
|
||||
buttonText: 'Get Quote',
|
||||
@@ -383,12 +383,12 @@ describe('后端微官网模块 (modules/card) - Bento Grid 便当盒组件树
|
||||
const created = await service.saveBlock({
|
||||
cardId: 100,
|
||||
type: 'article',
|
||||
title: '出海白皮书',
|
||||
title: '全球商务白皮书',
|
||||
colSpan: 1,
|
||||
rowSpan: 1,
|
||||
});
|
||||
expect(created.id).toBeDefined();
|
||||
expect(created.title).toBe('出海白皮书');
|
||||
expect(created.title).toBe('全球商务白皮书');
|
||||
});
|
||||
|
||||
it('deleteBlock 应能成功删除单个块', async () => {
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('Point 26: 国际商务日程预约排期与 ICS 日历订阅 (RFC 5545
|
||||
const mockMeeting: MeetingEventOptions = {
|
||||
uid: 'omnicard-meet-20260920-001@omnicard.ai',
|
||||
summary: 'OmniCard AI 商务拓展战略会谈 (Alex & Partner)',
|
||||
description: '讨论海外微官网落地方案, NFC 实体卡发行与访客雷达 CRM 对接。\n请准时参会。',
|
||||
description: '讨论国际微官网落地方案, NFC 实体卡发行与访客雷达 CRM 对接。\n请准时参会。',
|
||||
location: 'Zoom 会议室 (ID: 888-999-000), 新加坡滨海湾金融中心',
|
||||
startTime: new Date(Date.UTC(2026, 8, 20, 14, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2026, 8, 20, 15, 0, 0)),
|
||||
@@ -122,7 +122,7 @@ describe('Point 26: 国际商务日程预约排期与 ICS 日历订阅 (RFC 5545
|
||||
expect(ics).toContain('DTSTART:20260920T140000Z\r\n');
|
||||
expect(ics).toContain('DTEND:20260920T150000Z\r\n');
|
||||
expect(ics).toContain('SUMMARY:OmniCard AI 商务拓展战略会谈 (Alex & Partner)\r\n');
|
||||
expect(ics).toContain('DESCRIPTION:讨论海外微官网落地方案\\, NFC 实体卡发行与访客雷达 CRM 对接。\\n请准时参会。\r\n');
|
||||
expect(ics).toContain('DESCRIPTION:讨论国际微官网落地方案\\, NFC 实体卡发行与访客雷达 CRM 对接。\\n请准时参会。\r\n');
|
||||
expect(ics).toContain('LOCATION:Zoom 会议室 (ID: 888-999-000)\\, 新加坡滨海湾金融中心\r\n');
|
||||
expect(ics).toContain('ORGANIZER;CN=Alex Zheng:mailto:alex@omnicard.ai\r\n');
|
||||
expect(ics).toContain('URL:https://omnicard.ai/p/alex-zheng\r\n');
|
||||
@@ -193,10 +193,10 @@ describe('Point 26: 国际商务日程预约排期与 ICS 日历订阅 (RFC 5545
|
||||
portalController.ctx = mockCtx;
|
||||
|
||||
const result = await portalController.meetingIcs({
|
||||
summary: '全球合作伙伴出海研讨会',
|
||||
summary: '全球合作伙伴研讨会',
|
||||
startTime: '2026-09-22T09:00:00Z',
|
||||
endTime: '2026-09-22T10:00:00Z',
|
||||
description: '探讨 2026 年品牌海外本土化与数字名片生态布局',
|
||||
description: '探讨 2026 年品牌全球本土化与数字名片生态布局',
|
||||
location: '线上 Google Meet',
|
||||
organizerName: 'Alex Zheng',
|
||||
organizerEmail: 'alex@omnicard.ai',
|
||||
@@ -211,7 +211,7 @@ describe('Point 26: 国际商务日程预约排期与 ICS 日历订阅 (RFC 5545
|
||||
const icsOutput = (responseBody as string) || (result as string);
|
||||
expect(typeof icsOutput).toBe('string');
|
||||
expect(icsOutput).toContain('BEGIN:VCALENDAR');
|
||||
expect(icsOutput).toContain('SUMMARY:全球合作伙伴出海研讨会');
|
||||
expect(icsOutput).toContain('SUMMARY:全球合作伙伴研讨会');
|
||||
expect(icsOutput).toContain('DTSTART:20260922T090000Z');
|
||||
expect(icsOutput).toContain('DTEND:20260922T100000Z');
|
||||
expect(icsOutput).toContain('ORGANIZER;CN=Alex Zheng:mailto:alex@omnicard.ai');
|
||||
@@ -266,8 +266,8 @@ describe('Point 26: 国际商务日程预约排期与 ICS 日历订阅 (RFC 5545
|
||||
it('应正确从 ICS 字符串反向提取会议核心属性', () => {
|
||||
const meeting: MeetingEventOptions = {
|
||||
uid: 'roundtrip-test-001@omnicard.ai',
|
||||
summary: '出海战略合作洽谈',
|
||||
description: '讨论微官网与海外本地化支付通道',
|
||||
summary: '全球战略合作洽谈',
|
||||
description: '讨论微官网与全球本地化支付通道',
|
||||
location: '线上 Google Meet',
|
||||
startTime: '2026-09-25T08:00:00Z',
|
||||
endTime: '2026-09-25T09:00:00Z',
|
||||
@@ -282,8 +282,8 @@ describe('Point 26: 国际商务日程预约排期与 ICS 日历订阅 (RFC 5545
|
||||
const parsed = parseIcsEvent(ics);
|
||||
|
||||
expect(parsed.uid).toBe('roundtrip-test-001@omnicard.ai');
|
||||
expect(parsed.summary).toBe('出海战略合作洽谈');
|
||||
expect(parsed.description).toBe('讨论微官网与海外本地化支付通道');
|
||||
expect(parsed.summary).toBe('全球战略合作洽谈');
|
||||
expect(parsed.description).toBe('讨论微官网与全球本地化支付通道');
|
||||
expect(parsed.location).toBe('线上 Google Meet');
|
||||
expect(parsed.status).toBe('CONFIRMED');
|
||||
expect(typeof parsed.organizer).toBe('object');
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
adminCardCasesCurdOption,
|
||||
} from '../../../src/modules/card/controller/admin/cases';
|
||||
|
||||
describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
describe('后台案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
const entityFile = path.join(
|
||||
__dirname,
|
||||
'../../../src/modules/card/entity/cases.ts'
|
||||
@@ -59,10 +59,10 @@ describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
});
|
||||
|
||||
describe('2. 实体字段契约 (CardCasesEntity)', () => {
|
||||
it('实体实例应支持出海案例必需契约字段', () => {
|
||||
it('实体实例应支持案例必需契约字段', () => {
|
||||
const item = new CardCasesEntity();
|
||||
item.cardId = 1001;
|
||||
item.title = '智能电动滑板车欧美 DTC 出海';
|
||||
item.title = '智能电动滑板车欧美 DTC 全球拓展';
|
||||
item.client = 'VoltRide Global';
|
||||
item.duration = '2024.01 - 2024.06';
|
||||
item.metrics = '+320% ROI, 45,000+ 留资量';
|
||||
@@ -72,7 +72,7 @@ describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
item.sortOrder = 1;
|
||||
|
||||
expect(item.cardId).toBe(1001);
|
||||
expect(item.title).toBe('智能电动滑板车欧美 DTC 出海');
|
||||
expect(item.title).toBe('智能电动滑板车欧美 DTC 全球拓展');
|
||||
expect(item.client).toBe('VoltRide Global');
|
||||
expect(item.duration).toBe('2024.01 - 2024.06');
|
||||
expect(item.metrics).toBe('+320% ROI, 45,000+ 留资量');
|
||||
@@ -92,7 +92,7 @@ describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
Object.assign(new CardCasesEntity(), {
|
||||
id: 1,
|
||||
cardId: 101,
|
||||
title: '跨境智能硬件众筹出海',
|
||||
title: '全球智能硬件众筹发布',
|
||||
client: 'TechNova',
|
||||
duration: '3个月',
|
||||
metrics: '+280% 众筹目标达成',
|
||||
@@ -103,7 +103,7 @@ describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
Object.assign(new CardCasesEntity(), {
|
||||
id: 2,
|
||||
cardId: 101,
|
||||
title: '东南亚 TikTok 跨境直播获客',
|
||||
title: '东南亚 TikTok 国际直播获客',
|
||||
client: 'FashionGo',
|
||||
duration: '6个月',
|
||||
metrics: '千万级曝光, +150% GMV',
|
||||
@@ -163,9 +163,9 @@ describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
it('getCasesByCardId 应根据名片 cardId 筛选并按 sortOrder 升序排列', async () => {
|
||||
const cases = await service.getCasesByCardId(101);
|
||||
expect(cases).toHaveLength(2);
|
||||
expect(cases[0].title).toBe('东南亚 TikTok 跨境直播获客');
|
||||
expect(cases[0].title).toBe('东南亚 TikTok 国际直播获客');
|
||||
expect(cases[0].sortOrder).toBe(1);
|
||||
expect(cases[1].title).toBe('跨境智能硬件众筹出海');
|
||||
expect(cases[1].title).toBe('全球智能硬件众筹发布');
|
||||
expect(cases[1].sortOrder).toBe(2);
|
||||
});
|
||||
|
||||
@@ -177,7 +177,7 @@ describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
it('创建案例并能查询出最新案例', async () => {
|
||||
const created = await service.cardCasesEntity.save({
|
||||
cardId: 101,
|
||||
title: '拉美电商本土化品牌出海',
|
||||
title: '拉美电商本土化品牌打造',
|
||||
client: 'LatamDirect',
|
||||
duration: '4个月',
|
||||
metrics: '+90% 复购率',
|
||||
@@ -187,7 +187,7 @@ describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
||||
});
|
||||
|
||||
expect(created.id).toBeDefined();
|
||||
expect(created.title).toBe('拉美电商本土化品牌出海');
|
||||
expect(created.title).toBe('拉美电商本土化品牌打造');
|
||||
|
||||
const found = await service.cardCasesEntity.findOneBy({ id: created.id });
|
||||
expect(found).not.toBeNull();
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('Task 08: 后端公开短链路由与秒级微官网详情 API (Card Po
|
||||
themeCode: 'corporate_navy',
|
||||
// 敏感内部数据
|
||||
userId: 9999,
|
||||
remark: '重点出海客户核心高管,年费 VIP 专属',
|
||||
remark: '重点合作客户核心高管,年费 VIP 专属',
|
||||
internalConfig: '{"rateLimit": 1000, "debug": true, "internalTenantSecret": "sk_live_xyz"}',
|
||||
createTime: new Date(),
|
||||
updateTime: new Date(),
|
||||
@@ -71,7 +71,7 @@ describe('Task 08: 后端公开短链路由与秒级微官网详情 API (Card Po
|
||||
colSpan: 1,
|
||||
rowSpan: 1,
|
||||
sortOrder: 3,
|
||||
title: '出海 SaaS 落地指南',
|
||||
title: '全球 SaaS 落地指南',
|
||||
content: '深度解读全球数字名片与微官网搭建...',
|
||||
status: 1, // 有效
|
||||
internalNotes: '文章需由法务团队二次审核', // 敏感
|
||||
@@ -84,7 +84,7 @@ describe('Task 08: 后端公开短链路由与秒级微官网详情 API (Card Po
|
||||
rowSpan: 1,
|
||||
sortOrder: 1,
|
||||
title: '关于我',
|
||||
content: '10 年全球出海商务拓展经验',
|
||||
content: '10 年全球商务拓展经验',
|
||||
status: 1, // 有效
|
||||
internalNotes: '置顶 Hero 块',
|
||||
} as CardBentoEntity,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
PosterElementBox,
|
||||
} from '../../../src/modules/card/service/poster-layout';
|
||||
|
||||
describe('Point 54: 出海展会多语言电子邀请海报轻量级排版引擎', () => {
|
||||
describe('Point 54: 商务展会多语言电子邀请海报轻量级排版引擎', () => {
|
||||
let engine: CardPosterLayoutEngine;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { MOCK_CARDS_DATA, MockCardBundle } from '../../../src/modules/card/mock/seed-cards';
|
||||
import { CardPortalService } from '../../../src/modules/card/service/portal';
|
||||
import { CardInfoEntity } from '../../../src/modules/card/entity/info';
|
||||
import { CardBentoEntity } from '../../../src/modules/card/entity/bento';
|
||||
|
||||
describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试', () => {
|
||||
it('1. 应包含至少 5 组不同国际行业的高保真名片数据包', () => {
|
||||
expect(Array.isArray(MOCK_CARDS_DATA)).toBe(true);
|
||||
expect(MOCK_CARDS_DATA.length).toBeGreaterThanOrEqual(5);
|
||||
|
||||
const slugs = MOCK_CARDS_DATA.map(item => item.card.slug);
|
||||
const uniqueSlugs = new Set(slugs);
|
||||
expect(uniqueSlugs.size).toBe(MOCK_CARDS_DATA.length); // Slug 必须唯一
|
||||
});
|
||||
|
||||
it('2. 每张名片的基础信息必须完整且配有高分辨率头像与国际化联系方式', () => {
|
||||
MOCK_CARDS_DATA.forEach((bundle: MockCardBundle) => {
|
||||
const { card } = bundle;
|
||||
expect(card.id).toBeDefined();
|
||||
expect(card.name).toBeTruthy();
|
||||
expect(card.title).toBeTruthy();
|
||||
expect(card.company).toBeTruthy();
|
||||
expect(card.slogan).toBeTruthy();
|
||||
expect(card.status).toBe('published');
|
||||
|
||||
// 配图验证:头像必须是合法图片 URL (http/https)
|
||||
expect(card.avatarUrl).toMatch(/^https?:\/\/.+\.(png|jpe?g|webp|svg)(\?.*)?$/i);
|
||||
|
||||
// 国际化联系方式
|
||||
expect(card.phone).toMatch(/^\+[1-9]\d{1,14}$/); // E.164 规范
|
||||
expect(card.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
|
||||
expect(card.timezone).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('3. 每张名片必须包含有效的视频 Bento 块,且配有可播放视频流与专属海报封面', () => {
|
||||
MOCK_CARDS_DATA.forEach((bundle: MockCardBundle) => {
|
||||
const { bentoBlocks } = bundle;
|
||||
expect(Array.isArray(bentoBlocks)).toBe(true);
|
||||
expect(bentoBlocks.length).toBeGreaterThan(0);
|
||||
|
||||
// 必须包含至少一个 video 类型的 Bento 块
|
||||
const videoBlocks = bentoBlocks.filter(b => b.type === 'video');
|
||||
expect(videoBlocks.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
videoBlocks.forEach(videoBlock => {
|
||||
expect(videoBlock.title).toBeTruthy();
|
||||
// mediaUrl 作为视频封面海报,必须是合法图片 URL
|
||||
expect(videoBlock.mediaUrl).toMatch(/^https?:\/\/.+\.(png|jpe?g|webp)(\?.*)?$/i);
|
||||
// linkUrl 或 metaJson.videoUrl 必须是可播放的视频源 (mp4/m3u8/webm)
|
||||
let videoUrl = videoBlock.linkUrl;
|
||||
if (typeof videoBlock.metaJson === 'object' && videoBlock.metaJson !== null) {
|
||||
videoUrl = (videoBlock.metaJson as any).videoUrl || videoUrl;
|
||||
} else if (typeof videoBlock.metaJson === 'string') {
|
||||
const parsed = JSON.parse(videoBlock.metaJson);
|
||||
videoUrl = parsed.videoUrl || videoUrl;
|
||||
}
|
||||
expect(videoUrl).toMatch(/^https?:\/\/.+\.(mp4|webm|m3u8)(\?.*)?$/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('4. 每张名片必须包含图文案例与文章,且均配置了真实多媒体封面配图', () => {
|
||||
MOCK_CARDS_DATA.forEach((bundle: MockCardBundle) => {
|
||||
// 检查案例
|
||||
expect(Array.isArray(bundle.cases)).toBe(true);
|
||||
expect(bundle.cases.length).toBeGreaterThanOrEqual(1);
|
||||
bundle.cases.forEach(c => {
|
||||
expect(c.title).toBeTruthy();
|
||||
expect(c.client).toBeTruthy();
|
||||
expect(c.coverImage).toMatch(/^https?:\/\/.+\.(png|jpe?g|webp)(\?.*)?$/i);
|
||||
});
|
||||
|
||||
// 检查文章
|
||||
expect(Array.isArray(bundle.articles)).toBe(true);
|
||||
expect(bundle.articles.length).toBeGreaterThanOrEqual(1);
|
||||
bundle.articles.forEach(a => {
|
||||
expect(a.title).toBeTruthy();
|
||||
expect(a.coverImage).toMatch(/^https?:\/\/.+\.(png|jpe?g|webp)(\?.*)?$/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('5. 微官网服务 (CardPortalService) 能够无缝聚合各名片详情,正确渲染视频与图文块', async () => {
|
||||
const cardInfoRepo: any = {
|
||||
findOne: jest.fn().mockImplementation(async options => {
|
||||
const slug = options?.where?.slug;
|
||||
const id = options?.where?.id;
|
||||
const found = MOCK_CARDS_DATA.find(
|
||||
b => (slug && b.card.slug === slug) || (id && b.card.id === Number(id))
|
||||
);
|
||||
return found ? ({ ...found.card } as CardInfoEntity) : null;
|
||||
}),
|
||||
};
|
||||
|
||||
const cardBentoRepo: any = {
|
||||
find: jest.fn().mockImplementation(async options => {
|
||||
const cardId = options?.where?.cardId;
|
||||
const found = MOCK_CARDS_DATA.find(b => b.card.id === Number(cardId));
|
||||
return found ? found.bentoBlocks.map(blk => ({ ...blk } as CardBentoEntity)) : [];
|
||||
}),
|
||||
};
|
||||
|
||||
const portalService = new CardPortalService();
|
||||
portalService.cardInfoEntity = cardInfoRepo;
|
||||
portalService.cardBentoEntity = cardBentoRepo;
|
||||
CardPortalService.clearAllCache();
|
||||
|
||||
for (const bundle of MOCK_CARDS_DATA) {
|
||||
const detail = await portalService.getPortalDetail(bundle.card.slug);
|
||||
expect(detail).toBeDefined();
|
||||
expect(detail.name).toBe(bundle.card.name);
|
||||
expect(detail.avatarUrl).toBe(bundle.card.avatarUrl);
|
||||
|
||||
// 验证视频块是否存在于公开微官网聚合结果中
|
||||
const videoBlock = detail.bentoBlocks.find(b => b.type === 'video');
|
||||
expect(videoBlock).toBeDefined();
|
||||
expect(videoBlock?.mediaUrl).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ChannelAttributionEvent,
|
||||
} from '../../../src/modules/crm/service/attribution';
|
||||
|
||||
describe('Point 46: 访客雷达多渠道来源归因与出海转化漏斗分析', () => {
|
||||
describe('Point 46: 访客雷达多渠道来源归因与转化漏斗分析', () => {
|
||||
let service: RadarAttributionService;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -18,7 +18,7 @@ describe('Point 46: 访客雷达多渠道来源归因与出海转化漏斗分析
|
||||
expect(normalizeTrafficChannel('booth', 'qrcode')).toBe('booth_qr');
|
||||
});
|
||||
|
||||
it('应精准识别 WhatsApp 与 LinkedIn 社交出海渠道', () => {
|
||||
it('应精准识别 WhatsApp 与 LinkedIn 国际社交渠道', () => {
|
||||
expect(normalizeTrafficChannel('whatsapp', 'im')).toBe('whatsapp_chat');
|
||||
expect(normalizeTrafficChannel('linkedin', 'social')).toBe('linkedin_share');
|
||||
expect(normalizeTrafficChannel('email', 'sig')).toBe('email_signature');
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
TrafficFilterResult,
|
||||
} from '../../../src/modules/crm/service/bot-filter';
|
||||
|
||||
describe('Point 63 (P2): 访客雷达异常流量与出海社交抓取机器爬虫清洗过滤', () => {
|
||||
describe('Point 63 (P2): 访客雷达异常流量与社交抓取机器爬虫清洗过滤', () => {
|
||||
let service: RadarBotFilterService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AdminCrmLeadController } from '../../../src/modules/crm/controller/admi
|
||||
import { CrmLeadEntity } from '../../../src/modules/crm/entity/lead';
|
||||
import { RadarTrack } from '../../../src/comm/types/omni';
|
||||
|
||||
describe('【BTP-20】线索流转自动化与出海 Webhook / 实时推送集成测试', () => {
|
||||
describe('【BTP-20】线索流转自动化与外部 Webhook / 实时推送集成测试', () => {
|
||||
let syncService: CrmSyncService;
|
||||
let pushService: CrmRadarPushService;
|
||||
let mockLead: CrmLeadEntity;
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
GdprUserDataBundle,
|
||||
} from '../../../src/modules/crm/service/compliance';
|
||||
|
||||
describe('Point 53: 海外合规 GDPR / CCPA 访客数据匿名化脱敏与被遗忘权导出', () => {
|
||||
describe('Point 53: 国际合规 GDPR / CCPA 访客数据匿名化脱敏与被遗忘权导出', () => {
|
||||
let service: ComplianceGdprService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
CompanyProfileResult,
|
||||
} from '../../../src/modules/crm/service/enrichment';
|
||||
|
||||
describe('Point 49: 出海潜客企业邮箱域名解析与商业主页自动富化', () => {
|
||||
describe('Point 49: 潜客企业邮箱域名解析与商业主页自动富化', () => {
|
||||
let service: LeadCompanyEnrichmentService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('Point 55: 全球访客雷达国家城市坐标聚合与热力散点引
|
||||
service = new RadarGeoHeatmapService();
|
||||
});
|
||||
|
||||
describe('1. resolveCityCoordinates (出海核心商务枢纽城市经纬度解析)', () => {
|
||||
describe('1. resolveCityCoordinates (全球核心商务枢纽城市经纬度解析)', () => {
|
||||
it('应精准解析迪拜、利雅得、新加坡与伦敦经纬度', () => {
|
||||
const dubai = resolveCityCoordinates('AE', 'Dubai');
|
||||
expect(dubai).toEqual([55.27, 25.20]);
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
LeadBehaviorTrigger,
|
||||
} from '../../../src/modules/crm/service/lead-automation';
|
||||
|
||||
describe('Point 58 (P1): 出海潜客线索行为自动化阶段推进引擎', () => {
|
||||
describe('Point 58 (P1): 潜客线索行为自动化阶段推进引擎', () => {
|
||||
let service: LeadStageAutomationService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('多租户安全闭环: 公开留资与雷达足迹自动绑定名片
|
||||
it('公开留资未显式传入 tenantId 时,必须根据 cardId 自动追溯绑定归属名片的 tenantId 和 userId', async () => {
|
||||
const lead = await leadService.submitLead({
|
||||
cardId: 101,
|
||||
name: '海外采购商',
|
||||
name: '国际采购商',
|
||||
phone: '+971-50-9876543',
|
||||
company: 'Dubai Trading LLC',
|
||||
});
|
||||
@@ -68,7 +68,7 @@ describe('多租户安全闭环: 公开留资与雷达足迹自动绑定名片
|
||||
const lead = await leadService.submitLead({
|
||||
cardId: 101,
|
||||
tenantId: 999,
|
||||
name: '海外采购商',
|
||||
name: '国际采购商',
|
||||
phone: '+971-50-9876543',
|
||||
} as any);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
LeadWebhookPayload,
|
||||
} from '../../../src/modules/crm/service/webhook-dispatcher';
|
||||
|
||||
describe('Point 51: 出海商机实时 Webhook 触发器与 HMAC-SHA256 签名分发', () => {
|
||||
describe('Point 51: 商机实时 Webhook 触发器与 HMAC-SHA256 签名分发', () => {
|
||||
let dispatcher: LeadWebhookDispatcher;
|
||||
const mockSecret = 'whsec_8f9a2b4c6e1d7g5h3j8k9l0m';
|
||||
const mockPayload: LeadWebhookPayload = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AdminCrmRadarController } from '../../../src/modules/crm/controller/admin/radar';
|
||||
import {
|
||||
buildWorkplaceOverview,
|
||||
calculateTrackIntentScore,
|
||||
CrmRadarService,
|
||||
} from '../../../src/modules/crm/service/radar';
|
||||
import { CrmTrackEntity } from '../../../src/modules/crm/entity/track';
|
||||
@@ -152,4 +153,97 @@ describe('商务概览工作台聚合 (getWorkplaceOverview)', () => {
|
||||
expect(res.data.kpis.viewCount).toBe(2);
|
||||
expect(controller.crmRadarService.getWorkplaceOverview).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('数据专家重构:科学意向度与会话降噪增强', () => {
|
||||
it('科学意向分算法 (calculateTrackIntentScore) 必须符合多因子阶梯模型', () => {
|
||||
// 1. 直链 0 秒跳出,无任何浏览与滚动:低意向分 (10分)
|
||||
const bounceScore = calculateTrackIntentScore({
|
||||
touchType: 'link',
|
||||
stayDurationMs: 0,
|
||||
scrollDepth: 0,
|
||||
viewedBlocks: [],
|
||||
});
|
||||
expect(bounceScore).toBe(10);
|
||||
|
||||
// 2. NFC 面对面触碰初始分:天然高权重 (35分)
|
||||
const nfcInitialScore = calculateTrackIntentScore({
|
||||
touchType: 'nfc',
|
||||
stayDurationMs: 0,
|
||||
});
|
||||
expect(nfcInitialScore).toBe(35);
|
||||
|
||||
// 3. QR 扫码 + 30秒研读 + 2个模块 + 深度滚动:高意向买家 (25 + 20 + 10 + 10 = 65分)
|
||||
const engagedScore = calculateTrackIntentScore({
|
||||
touchType: 'qr',
|
||||
stayDurationMs: 30000,
|
||||
viewedBlocks: ['hero', 'products'],
|
||||
scrollDepth: 75,
|
||||
});
|
||||
expect(engagedScore).toBe(65);
|
||||
|
||||
// 4. NFC 触碰 + 150秒深读 + 4个模块 + 深度滚动:极高意向买家 (封顶 99分)
|
||||
const hotScore = calculateTrackIntentScore({
|
||||
touchType: 'nfc',
|
||||
stayDurationMs: 150000,
|
||||
viewedBlocks: ['hero', 'products', 'cases', 'contact'],
|
||||
scrollDepth: 95,
|
||||
});
|
||||
expect(hotScore).toBe(99);
|
||||
});
|
||||
|
||||
it('buildWorkplaceOverview 应实现会话去噪与合并,并填充名片名称和本地标记', () => {
|
||||
const cardList = [
|
||||
{ id: 1, name: 'Alex Chen', title: '全球商务总监' },
|
||||
];
|
||||
|
||||
// 同一访客 vis_101 先产生了 0 秒 PV,又更新了 45 秒有效停留
|
||||
const t1 = track({
|
||||
id: 10,
|
||||
cardId: 1,
|
||||
touchType: 'link',
|
||||
country: 'Local',
|
||||
city: 'Local',
|
||||
stayDurationMs: 0,
|
||||
createTime: new Date('2026-09-21T10:00:00'),
|
||||
});
|
||||
t1.visitorId = 'vis_101';
|
||||
|
||||
const t2 = track({
|
||||
id: 11,
|
||||
cardId: 1,
|
||||
touchType: 'link',
|
||||
country: 'Local',
|
||||
city: 'Local',
|
||||
stayDurationMs: 45000,
|
||||
viewedBlocks: ['products'],
|
||||
createTime: new Date('2026-09-21T10:01:00'),
|
||||
});
|
||||
t2.visitorId = 'vis_101';
|
||||
|
||||
const result = buildWorkplaceOverview(
|
||||
[t1, t2],
|
||||
[],
|
||||
now,
|
||||
10,
|
||||
cardList
|
||||
);
|
||||
|
||||
// 验证 1:去噪合并后同一会话只保留 1 条记录
|
||||
expect(result.radarFeed).toHaveLength(1);
|
||||
const feedItem = result.radarFeed[0];
|
||||
|
||||
// 验证 2:停留时长采用合并后的最高值 45 秒
|
||||
expect(feedItem.stayDurationMs).toBe(45000);
|
||||
expect(feedItem.viewedBlocks).toContain('products');
|
||||
|
||||
// 验证 3:名片名称正确注入
|
||||
expect(feedItem.cardTitle).toBe('Alex Chen · 全球商务总监');
|
||||
|
||||
// 验证 4:识别为本地环境
|
||||
expect(feedItem.isLocal).toBe(true);
|
||||
|
||||
// 验证 5:意向分由后端自动算出并大于 0 秒跳出分
|
||||
expect(feedItem.intentScore).toBeGreaterThan(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
canTransitionBatchStatus,
|
||||
} from '../../../src/modules/nfc/service/manufacturing';
|
||||
|
||||
describe('Point 45: 跨境实体卡供应链生产批次工单生命周期与良率检验', () => {
|
||||
describe('Point 45: 国际实体卡供应链生产批次工单生命周期与良率检验', () => {
|
||||
let service: NfcManufacturingService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from '../../../src/modules/team/service/currency-tax';
|
||||
import { TaxCalculationResult } from '../../../src/comm/types/omni';
|
||||
|
||||
describe('Point 57 (P1): 跨境多币种汇率换算与出海发票税制计算', () => {
|
||||
describe('Point 57 (P1): 跨国多币种汇率换算与国际发票税制计算', () => {
|
||||
let service: CurrencyTaxCalculatorService;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -35,7 +35,7 @@ describe('Point 57 (P1): 跨境多币种汇率换算与出海发票税制计算'
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. calculateInternationalTax (出海合规发票税制计算)', () => {
|
||||
describe('2. calculateInternationalTax (国际合规发票税制计算)', () => {
|
||||
it('阿联酋企业或个人消费应按 5% VAT 计税', () => {
|
||||
const res = calculateInternationalTax({
|
||||
amount: 1000,
|
||||
@@ -50,7 +50,7 @@ describe('Point 57 (P1): 跨境多币种汇率换算与出海发票税制计算'
|
||||
expect(res.taxName).toBe('UAE VAT');
|
||||
});
|
||||
|
||||
it('欧盟企业间跨境 B2B (Reverse Charge) 应适用反向征税 0% VAT', () => {
|
||||
it('欧盟企业间跨国 B2B (Reverse Charge) 应适用反向征税 0% VAT', () => {
|
||||
const res = calculateInternationalTax({
|
||||
amount: 2000,
|
||||
currency: 'EUR',
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
assertLeadAccessPermission,
|
||||
} from '../../../src/modules/team/service/data-scope';
|
||||
|
||||
describe('Point 52: 出海多租户团队成员细粒度数据隔离与销售管辖范围保护', () => {
|
||||
describe('Point 52: 多租户团队成员细粒度数据隔离与销售管辖范围保护', () => {
|
||||
let service: TeamDataScopeService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('【财务与运营新账统计】TeamFinanceAnalyticsService 单元测
|
||||
expect(Array.isArray(result.planDistribution)).toBe(true);
|
||||
expect(result.planDistribution.some((item) => item.plan === 'pro')).toBe(true);
|
||||
|
||||
// 出海区域分布
|
||||
// 国际区域分布
|
||||
expect(Array.isArray(result.regionalRevenue)).toBe(true);
|
||||
|
||||
// 流水明细
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('Point 19: 离职资产交接工单流程与历史线索迁移 单元
|
||||
lead1.id = 1;
|
||||
lead1.userId = 1001;
|
||||
lead1.cardId = 101;
|
||||
lead1.name = '重点出海客户 A';
|
||||
lead1.name = '重点合作客户 A';
|
||||
lead1.isPool = false;
|
||||
lead1.status = 'contacted';
|
||||
leads.push(lead1);
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('Point 28: 展会现场团队获客实时排行榜大屏广播 (Live Ra
|
||||
u1.id = 101;
|
||||
u1.name = 'Alex Rivera';
|
||||
u1.tenantId = 1;
|
||||
u1.departmentName = '出海一队';
|
||||
u1.departmentName = '国际业务一队';
|
||||
|
||||
const c1 = new CardInfoEntity();
|
||||
c1.id = 1;
|
||||
@@ -103,7 +103,7 @@ describe('Point 28: 展会现场团队获客实时排行榜大屏广播 (Live Ra
|
||||
u2.id = 102;
|
||||
u2.name = 'Sarah Wang';
|
||||
u2.tenantId = 1;
|
||||
u2.departmentName = '出海二队';
|
||||
u2.departmentName = '国际业务二队';
|
||||
|
||||
const c2 = new CardInfoEntity();
|
||||
c2.id = 2;
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('Point 17: 后台组织架构、部门树与成员名片分配 (Team Me
|
||||
users = [];
|
||||
cards = [];
|
||||
|
||||
// 预置部门数据:总部 (1) -> 研发中心 (2) -> 前端组 (3); 总部 (1) -> 销售出海中心 (4)
|
||||
// 预置部门数据:总部 (1) -> 研发中心 (2) -> 前端组 (3); 总部 (1) -> 国际业务拓展部 (4)
|
||||
const d1 = new BaseSysDepartmentEntity();
|
||||
d1.id = 1;
|
||||
d1.name = '全球商务总部';
|
||||
@@ -56,7 +56,7 @@ describe('Point 17: 后台组织架构、部门树与成员名片分配 (Team Me
|
||||
|
||||
const d2 = new BaseSysDepartmentEntity();
|
||||
d2.id = 2;
|
||||
d2.name = '海外研发中心';
|
||||
d2.name = '国际研发中心';
|
||||
d2.parentId = 1;
|
||||
d2.orderNum = 10;
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('Point 17: 后台组织架构、部门树与成员名片分配 (Team Me
|
||||
|
||||
const d4 = new BaseSysDepartmentEntity();
|
||||
d4.id = 4;
|
||||
d4.name = '出海业务拓展部';
|
||||
d4.name = '国际业务拓展部';
|
||||
d4.parentId = 1;
|
||||
d4.orderNum = 20;
|
||||
|
||||
@@ -201,7 +201,7 @@ describe('Point 17: 后台组织架构、部门树与成员名片分配 (Team Me
|
||||
expect(root.title).toBe('全球商务总部');
|
||||
expect(root.children?.length).toBe(2);
|
||||
|
||||
// children 排序应为 海外研发中心 (orderNum: 10) 和 出海业务拓展部 (orderNum: 20)
|
||||
// children 排序应为 国际研发中心 (orderNum: 10) 和 国际业务拓展部 (orderNum: 20)
|
||||
expect(root.children?.[0].id).toBe(2);
|
||||
expect(root.children?.[0].children?.length).toBe(1); // 前端组 (3)
|
||||
expect(root.children?.[0].children?.[0].id).toBe(3);
|
||||
@@ -211,7 +211,7 @@ describe('Point 17: 后台组织架构、部门树与成员名片分配 (Team Me
|
||||
// 人数统计验证
|
||||
expect(root.memberCount).toBe(1); // u1 在总部
|
||||
expect(root.children?.[0].children?.[0].memberCount).toBe(1); // u2 在前端组
|
||||
expect(root.children?.[1].memberCount).toBe(1); // u3 在出海业务部
|
||||
expect(root.children?.[1].memberCount).toBe(1); // u3 在国际业务部
|
||||
});
|
||||
|
||||
it('2.2 addDepartment 校验部门名称必填并成功入库', async () => {
|
||||
|
||||
@@ -34,6 +34,15 @@ type ArticleDetailState = {
|
||||
description: string;
|
||||
image: string;
|
||||
};
|
||||
authorName?: string;
|
||||
authorTitle?: string;
|
||||
authorCompany?: string;
|
||||
authorAvatar?: string;
|
||||
readTimeMinutes?: number;
|
||||
summaryText?: string;
|
||||
scrollDepth?: number;
|
||||
enterTime?: number;
|
||||
visitorId?: string;
|
||||
};
|
||||
|
||||
type ArticleShareFields = {
|
||||
@@ -65,6 +74,15 @@ type ArticleDetailModule = {
|
||||
getArticleShareFields: (state: ArticleDetailState, pageUrl: string) => ArticleShareFields;
|
||||
loadArticleDetail: (state: ArticleDetailState) => Promise<BentoBlockFixture | null>;
|
||||
copyArticleShareLink: (state: ArticleDetailState, pageUrl?: string) => boolean;
|
||||
resolveArticleDetailText: (key: string, locale?: string) => string;
|
||||
updateArticleScrollDepth: (
|
||||
state: ArticleDetailState,
|
||||
scrollTop: number,
|
||||
scrollHeight: number,
|
||||
windowHeight: number
|
||||
) => number;
|
||||
reportArticleStay: (state: ArticleDetailState, scrollDepth?: number) => Promise<unknown>;
|
||||
navToAuthorPortal: (state: ArticleDetailState) => void;
|
||||
};
|
||||
|
||||
type UniToastOption = {
|
||||
@@ -453,4 +471,112 @@ describe('OmniCard AI 用户端文章详情业务逻辑测试 (useArticleDetail.
|
||||
expect(toastCalls.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('4. 专栏作者微展台与核心摘要导读', () => {
|
||||
it('4.1 加载文章时能一并解析名片作者信息与核心摘要', async () => {
|
||||
const {
|
||||
createArticleDetailState,
|
||||
applyArticleDetailOptions,
|
||||
loadArticleDetail,
|
||||
} = loadUseArticleDetail();
|
||||
|
||||
const state = createArticleDetailState();
|
||||
applyArticleDetailOptions(state, { cardId: 'card-101', blockId: '3' });
|
||||
await loadArticleDetail(state);
|
||||
|
||||
expect(state.authorName).toBe('Alex Vance');
|
||||
expect(state.authorTitle).toBe('Managing Director & Global Partner');
|
||||
expect(state.authorCompany).toBe('Nexus Tech Global Ltd');
|
||||
expect(state.readTimeMinutes).toBeGreaterThanOrEqual(1);
|
||||
expect(state.summaryText?.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('5. 阅读滑屏深度计算与雷达停留时长上报', () => {
|
||||
it('5.1 updateArticleScrollDepth 正确计算并单调递增滑屏百分比', () => {
|
||||
const { createArticleDetailState, updateArticleScrollDepth } = loadUseArticleDetail();
|
||||
const state = createArticleDetailState();
|
||||
|
||||
expect(state.scrollDepth).toBe(0);
|
||||
// 顶部
|
||||
updateArticleScrollDepth(state, 0, 2000, 600);
|
||||
expect(state.scrollDepth).toBe(0);
|
||||
|
||||
// 中间 (scrollTop = 700 / maxScroll = 1400 => 50%)
|
||||
updateArticleScrollDepth(state, 700, 2000, 600);
|
||||
expect(state.scrollDepth).toBe(50);
|
||||
|
||||
// 向上回滚时保持最大深度
|
||||
updateArticleScrollDepth(state, 200, 2000, 600);
|
||||
expect(state.scrollDepth).toBe(50);
|
||||
|
||||
// 触底 (100%)
|
||||
updateArticleScrollDepth(state, 1400, 2000, 600);
|
||||
expect(state.scrollDepth).toBe(100);
|
||||
});
|
||||
|
||||
it('5.2 reportArticleStay 将停留时长与阅读深度正确上报至雷达', async () => {
|
||||
const {
|
||||
createArticleDetailState,
|
||||
applyArticleDetailOptions,
|
||||
loadArticleDetail,
|
||||
reportArticleStay,
|
||||
} = loadUseArticleDetail();
|
||||
|
||||
const state = createArticleDetailState();
|
||||
applyArticleDetailOptions(state, { cardId: 'card-101', blockId: '3' });
|
||||
await loadArticleDetail(state);
|
||||
|
||||
mockRequestResponse = {
|
||||
statusCode: 200,
|
||||
data: {
|
||||
code: 1000,
|
||||
message: 'success',
|
||||
data: {
|
||||
id: 'track-1',
|
||||
cardId: 'card-101',
|
||||
visitorId: 'vis-test',
|
||||
touchType: 'link',
|
||||
stayDurationMs: 1200,
|
||||
scrollDepth: 75,
|
||||
viewedBlocks: ['3'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
state.enterTime = Date.now() - 1500;
|
||||
state.scrollDepth = 75;
|
||||
|
||||
await reportArticleStay(state, 75);
|
||||
|
||||
expect(lastRequestOptions?.url).toContain('/open/crm/radar/track');
|
||||
const body = lastRequestOptions?.data as Record<string, unknown>;
|
||||
expect(body.cardId).toBe('card-101');
|
||||
expect(body.touchType).toBe('link');
|
||||
expect(body.scrollDepth).toBe(75);
|
||||
expect(body.viewedBlocks).toEqual(['3']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('6. 多语言本地化文案体系', () => {
|
||||
it('6.1 覆盖 zh-CN, en-US, ar-SA, es-ES 等语种的核心文案', () => {
|
||||
const { resolveArticleDetailText } = loadUseArticleDetail();
|
||||
|
||||
expect(resolveArticleDetailText('kicker', 'zh-CN')).toBe('深度洞见');
|
||||
expect(resolveArticleDetailText('summary', 'zh-CN')).toBe('核心导读');
|
||||
expect(resolveArticleDetailText('author', 'zh-CN')).toBe('专栏作者');
|
||||
expect(resolveArticleDetailText('viewPortal', 'zh-CN')).toBe('查看微官网');
|
||||
|
||||
expect(resolveArticleDetailText('kicker', 'en-US')).toBe('Thought Leadership');
|
||||
expect(resolveArticleDetailText('summary', 'en-US')).toBe('Key Takeaways');
|
||||
expect(resolveArticleDetailText('author', 'en-US')).toBe('Author');
|
||||
expect(resolveArticleDetailText('viewPortal', 'en-US')).toBe('View Profile');
|
||||
|
||||
expect(resolveArticleDetailText('kicker', 'ar-SA')).toBe('رؤى متعمقة');
|
||||
expect(resolveArticleDetailText('summary', 'ar-SA')).toBe('أبرز النقاط');
|
||||
|
||||
expect(resolveArticleDetailText('kicker', 'es-ES')).toBe('Perspectivas');
|
||||
expect(resolveArticleDetailText('summary', 'es-ES')).toBe('Puntos clave');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,6 +47,12 @@ type MockBentoBlock = {
|
||||
metrics?: CaseKpi[];
|
||||
pdfUrl?: string;
|
||||
youtubeUrl?: string;
|
||||
client?: string;
|
||||
industry?: string;
|
||||
region?: string;
|
||||
challenge?: string;
|
||||
solution?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -89,6 +95,8 @@ type UseCaseDetailModule = {
|
||||
buildExchangePath: (cardId: string) => string;
|
||||
openCaseExchange: (cardId: string) => void;
|
||||
goCaseBack: () => void;
|
||||
copyCaseExternalLink: (url: string) => boolean;
|
||||
resolveCaseDetailText: (key: string) => string;
|
||||
};
|
||||
|
||||
function loadUseCaseDetail(): UseCaseDetailModule {
|
||||
@@ -371,4 +379,72 @@ describe('OmniCard AI 用户端案例详情业务逻辑测试 (useCaseDetail.uts
|
||||
expect(navigateBackCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('5. Bento Grid 模块化结构化数据与国际化解析', () => {
|
||||
it('5.1 支持从 metaJson 解析 client, industry, region, challenge, solution', () => {
|
||||
const { createCaseDetailState, applyCaseDetailOptions, applyPortalCase } =
|
||||
loadUseCaseDetail();
|
||||
const card = buildMockPortal();
|
||||
card.bentoBlocks[1].metaJson = {
|
||||
client: 'Global Logistics Corp',
|
||||
industry: 'Supply Chain IoT',
|
||||
region: 'Middle East & North Africa',
|
||||
challenge: 'High latency in cross-border tracking and data silos.',
|
||||
solution: 'Deployed dual-frequency NFC sensors and cloud-native mesh.',
|
||||
kpis: [{ label: 'On-time Delivery', value: '99.8%' }],
|
||||
};
|
||||
|
||||
const state = createCaseDetailState();
|
||||
applyCaseDetailOptions(state, { cardId: 'card-101', blockId: '2' });
|
||||
applyPortalCase(state, card);
|
||||
|
||||
expect((state as any).client).toBe('Global Logistics Corp');
|
||||
expect((state as any).industry).toBe('Supply Chain IoT');
|
||||
expect((state as any).region).toBe('Middle East & North Africa');
|
||||
expect((state as any).challenge).toBe(
|
||||
'High latency in cross-border tracking and data silos.'
|
||||
);
|
||||
expect((state as any).solution).toBe(
|
||||
'Deployed dual-frequency NFC sensors and cloud-native mesh.'
|
||||
);
|
||||
expect(state.kpis).toEqual([{ label: 'On-time Delivery', value: '99.8%' }]);
|
||||
});
|
||||
|
||||
it('5.2 resolveCaseDetailText 正确解析新增的 Bento 模组词条', () => {
|
||||
const { resolveCaseDetailText } = loadUseCaseDetail();
|
||||
|
||||
expect(resolveCaseDetailText('badge')).toBeDefined();
|
||||
expect(resolveCaseDetailText('overview')).toBeDefined();
|
||||
expect(resolveCaseDetailText('challenge')).toBeDefined();
|
||||
expect(resolveCaseDetailText('solution')).toBeDefined();
|
||||
expect(resolveCaseDetailText('kpis')).toBeDefined();
|
||||
expect(resolveCaseDetailText('resources')).toBeDefined();
|
||||
expect(resolveCaseDetailText('pdfAction')).toBeDefined();
|
||||
expect(resolveCaseDetailText('videoAction')).toBeDefined();
|
||||
});
|
||||
|
||||
it('5.3 copyCaseExternalLink 触发 uni.setClipboardData 与 Toast 提示', () => {
|
||||
let clipboardText = '';
|
||||
let toastCalled = false;
|
||||
(global as { uni?: Record<string, unknown> }).uni = {
|
||||
setClipboardData: (opts: { data: string; success?: () => void }) => {
|
||||
clipboardText = opts.data;
|
||||
opts.success?.();
|
||||
},
|
||||
showToast: () => {
|
||||
toastCalled = true;
|
||||
},
|
||||
};
|
||||
|
||||
const { copyCaseExternalLink } = loadUseCaseDetail();
|
||||
const res = copyCaseExternalLink('https://cdn.omnicard.ai/doc.pdf');
|
||||
|
||||
expect(res).toBe(true);
|
||||
expect(clipboardText).toBe('https://cdn.omnicard.ai/doc.pdf');
|
||||
expect(toastCalled).toBe(true);
|
||||
|
||||
const emptyRes = copyCaseExternalLink('');
|
||||
expect(emptyRes).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 严格遵循 TDD 测试驱动规范
|
||||
*/
|
||||
|
||||
describe('Point 56: 移动端 App / H5 出海名片 Universal Link 深度链接路由分发解析器', () => {
|
||||
describe('Point 56: 移动端 App / H5 智能数字名片 Universal Link 深度链接路由分发解析器', () => {
|
||||
let router: any;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* OmniCard AI (CRMmp) - 访客端多语言动态切换与 RTL 镜像契约测试 (TDD)
|
||||
* 严格按照 AGENTS.md 与出海国际化 WCAG 标准规范
|
||||
* 严格按照 AGENTS.md 与国际化 WCAG 标准规范
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
@@ -173,7 +173,7 @@ describe('Point 04: 访客端多语言动态切换与 RTL 镜像无感适配', (
|
||||
});
|
||||
|
||||
describe('4. 默认 GeoIP 语言回退 (resolveGeoLocale)', () => {
|
||||
it('根据出海访客的国家码准确映射主流语言或回退 en-US', () => {
|
||||
it('根据访客的国家码准确映射主流语言或回退 en-US', () => {
|
||||
const { resolveGeoLocale } = require(LOCALE_UTS) as {
|
||||
resolveGeoLocale: (countryCode?: string, fallback?: string) => string;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* OmniCard AI (CRMmp) - 移动微官网离线 LocalStorage 极速预加载与骨架屏单测套件 (TDD)
|
||||
* 严格遵循 AGENTS.md 规范与出海高性能架构契约
|
||||
* 严格遵循 AGENTS.md 规范与高性能架构契约
|
||||
*/
|
||||
|
||||
export interface MockBentoBlock {
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('uni-app 框架自带原生 tabBar 规范测试 (TDD Red -> Green)', ()
|
||||
|
||||
// 关键规范:tabBar.list[0].pagePath 必须与 pages[0].path 一致,原生 tabBar 才能在首屏正常加载
|
||||
expect(config.tabBar.list[0].pagePath).toBe(config.pages[0].path);
|
||||
expect(config.tabBar.list[0].text).toBe('官网');
|
||||
expect(config.tabBar.list[0].text).toBe('名片');
|
||||
|
||||
// 校验颜色 Token
|
||||
expect(config.tabBar.color.toLowerCase()).toBe('#667085');
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
### 一、 产品战略定位与商业愿景 (Product Vision & Strategy)
|
||||
|
||||
#### 1.1 产品定位
|
||||
**OmniCard AI** 是一款面向**全球出海商务、跨国企业团队、自由职业者及国际会展场景**的新一代 **AI 驱动型智能数字名片 + 移动微官网 (Bento Micro-Site) + 潜客雷达 CRM + 实体 NFC 智能卡管家** 一体化 SaaS 平台。
|
||||
**OmniCard AI** 是一款面向**全球现代商务、跨国企业团队、自由职业者及国际会展场景**的新一代 **AI 驱动型智能数字名片 + 移动微官网 (Bento Micro-Site) + 潜客雷达 CRM + 实体 NFC 智能卡管家** 一体化 SaaS 平台。
|
||||
|
||||
它不仅是一张数字名片,更是出海企业与个人的**微型官网**与**24小时自动获客线索引擎**:
|
||||
它不仅是一张数字名片,更是全球企业与个人的**微型官网**与**24小时自动获客线索引擎**:
|
||||
- **用户端与微官网(User Client: App & H5)**:秒级首屏响应、无缝多语言自适应(含 RTL 排版)、Bento Grid 现代卡片美学、名片内置 24/7 AI 顾问对话、NFC 实体卡秒级碰触与离线 vCard 导入。
|
||||
- **管理后台前端(Admin Web)**:可视化名片搭建、AI 批量出海润色、NFC 硬件全生命周期下发管控、访客轨迹透视与 CRM 商机漏斗、多租户企业品牌管控。
|
||||
- **管理后台前端(Admin Web)**:可视化名片搭建、AI 批量本地化润色、NFC 硬件全生命周期下发管控、访客轨迹透视与 CRM 商机漏斗、多租户企业品牌管控。
|
||||
- **后端中枢服务(Backend API)**:基于 Cool-Admin (Node.js) 模块化架构,提供高性能企业级数据中枢、安全 RBAC 鉴权、微秒级雷达日志追踪及官方插件集成。
|
||||
|
||||
#### 1.2 核心用户画像 (Target Personas)
|
||||
1. **外贸/跨境出海销售与参展团队**:常年参与海外展会(CES、广交会、迪拜 GITEX 等),需要跨语种交流,通过手机或 NFC 实体卡一碰即达,零摩擦留存客户线索。
|
||||
1. **全球商务销售与国际参展团队**:常年参与国际知名展会(CES、IFA、迪拜 GITEX 等),需要跨语种交流,通过手机或 NFC 实体卡一碰即达,零摩擦留存客户线索。
|
||||
2. **跨国商务顾问/自由专家 (Consultants & Creators)**:需要集个人品牌展示、过往案例 (Portfolio)、干货博客文章 (Insights) 与预约排期于一体的移动端便当盒微官网。
|
||||
3. **出海科技企业 / 品牌出海团队**:需要统一管理数十到数百名商务人员的名片物料,统一品牌 VI 形象,并集中追踪团队名片带来的全球线索转化与公海池沉淀。
|
||||
3. **全球化科技企业 / 国际品牌团队**:需要统一管理数十到数百名商务人员的名片物料,统一品牌 VI 形象,并集中追踪团队名片带来的全球线索转化与公海池沉淀。
|
||||
|
||||
---
|
||||
|
||||
@@ -44,7 +44,7 @@ flowchart TD
|
||||
subgraph 后端服务_Cool-Admin [Backend API: Node.js Midway.js]
|
||||
CardModule[名片与微官网模块 modules/card]
|
||||
CRMModule[访客雷达与商机模块 modules/crm]
|
||||
AIModule[AI 对话与出海润色模块 modules/ai]
|
||||
AIModule[AI 对话与本地化润色模块 modules/ai]
|
||||
NFCModule[NFC 设备生命周期模块 modules/nfc]
|
||||
TeamModule[多租户与团队管理 modules/team]
|
||||
PluginEco[官方生态: OSS存储 / Redis缓存 / Task调度]
|
||||
@@ -121,7 +121,7 @@ flowchart TD
|
||||
|
||||
#### 3.4 模块四:轻量级客户关系 CRM 与访客雷达 (Lead CRM & Radar)
|
||||
1. **实时访客雷达 (Visitor Radar)**:
|
||||
- 海外客户在展会或拜访后点开名片,销售 App 收到即时推送(Push Notification):
|
||||
- 国际客商在展会或拜访后点开名片,销售 App 收到即时推送(Push Notification):
|
||||
*“来自阿联酋迪拜的访客正在浏览您的名片,已停留 3 分钟,正在重点查看‘新能源储能项目案例’!”*
|
||||
2. **无摩擦线索沉淀 (Frictionless Lead Exchange)**:
|
||||
- “互换名片”弹窗:访客快速输入姓名、公司、邮箱/WhatsApp,或拍照上传纸质名片(OCR 识别入库)。
|
||||
@@ -174,14 +174,14 @@ admin-web/ (Arco Pro 架构)
|
||||
| 模块名称 | 页面路由路径 | 页面中文名称 | 核心功能点与交互规范 | 推荐复用 Arco 官方组件 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **工作台** | `/dashboard/workplace` | 商务概览工作台 | 核心 KPI 概览(触碰量、浏览量、新线索、成交转化);今日实时雷达动态流;快捷入口(新建名片、发卡、线索导出)。 | `Grid`, `Statistic`, `Card`, `Timeline`, `Button` |
|
||||
| **数据大屏** | `/dashboard/radar-map` | 全球访客雷达大屏 | 全球出海访客分布地理热力图(国家/地区占比);各名片停留时长与访问峰值走势图;出海客户语言/设备终端分析。 | `Card`, `DatePicker.RangePicker`, `Spin`, `Empty` |
|
||||
| **数据大屏** | `/dashboard/radar-map` | 全球访客雷达大屏 | 全球访客分布地理热力图(国家/地区占比);各名片停留时长与访问峰值走势图;国际客户语言/设备终端分析。 | `Card`, `DatePicker.RangePicker`, `Spin`, `Empty` |
|
||||
| **名片管理** | `/card/list` | 数字名片资产库 | 名片表格与卡片双视图切换;发布/草稿状态;一键复制短链;动态防伪二维码下载(PNG/SVG/PDF打印格式);批量导出 vCard。 | `Table`, `Radio.Group`, `Badge`, `Popconfirm`, `Dropdown` |
|
||||
| **微官网搭建** | `/card/builder/:id` | Bento 可视化搭建器 | **左侧组件库**:Hero身份、社媒矩阵、案例卡、文章卡、视频音频、留资表单;<br>**中间画布**:手机端视口实时响应模拟(支持浅/深色、RTL阿语实时镜像);<br>**右侧属性配置**:模块属性、多语言文案切换、Bento 跨度调整 (1x1, 1x2, 2x1, 2x2)。 | `Layout`, `Tabs`, `Form`, `Slider`, `Upload`, `Switch`, `ColorPicker` |
|
||||
| **案例展厅** | `/card/cases` | 案例作品集管理 | 出海成功案例列表;多媒体素材管理(图片瀑布流、YouTube/Vimeo 链接);方案 PDF 白皮书上传;成果 KPI 标签打标。 | `Table`, `Modal`, `Form.Item`, `Upload`, `Tag` |
|
||||
| **案例展厅** | `/card/cases` | 案例作品集管理 | 标杆成功案例列表;多媒体素材管理(图片瀑布流、YouTube/Vimeo 链接);方案 PDF 白皮书上传;成果 KPI 标签打标。 | `Table`, `Modal`, `Form.Item`, `Upload`, `Tag` |
|
||||
| **文章动态** | `/card/articles` | 博客与动态发布 | 富文本文章发布;移动端排版预览;SEO 与 OpenGraph 社交预览卡(WhatsApp/LinkedIn)图片与描述自动生成。 | `Table`, `Input`, `Drawer`, `Alert` |
|
||||
| **AI 智能客服** | `/ai/concierge` | 名片 AI 顾问调优 | 数字人/顾问人设设定(外贸专家、商务代表等);欢迎语与引导词设置;高频 FAQ 手动配置;自动留资触发策略设置。 | `Form`, `Input.TextArea`, `Slider`, `Radio.Group`, `Card` |
|
||||
| **AI 智能客服** | `/ai/concierge` | 名片 AI 顾问调优 | 数字人/顾问人设设定(国际贸易顾问、商务代表等);欢迎语与引导词设置;高频 FAQ 手动配置;自动留资触发策略设置。 | `Form`, `Input.TextArea`, `Slider`, `Radio.Group`, `Card` |
|
||||
| **知识库 RAG** | `/ai/knowledge` | 知识库与向量切片 | 知识库文件批量上传(PDF/Docx/TXT/MD);URL 网站智能抓取;向量化进度条与分块切片预览;召回命中率调试沙盒。 | `Upload.Dragger`, `Progress`, `Table`, `Drawer`, `Input.Search` |
|
||||
| **出海润色** | `/ai/translation` | 多语言 AI 润色中心 | 批量多语言内容润色与地道化翻译;外贸专有名词与行业术语库 (Glossary) 维护;口吻风格切换(稳重/直接/故事感)。 | `Table`, `Select`, `Input`, `Button`, `Space` |
|
||||
| **智能润色** | `/ai/translation` | 多语言 AI 润色中心 | 批量多语言内容润色与地道化翻译;跨国专有名词与行业术语库 (Glossary) 维护;口吻风格切换(稳重/直接/故事感)。 | `Table`, `Select`, `Input`, `Button`, `Space` |
|
||||
| **访客轨迹** | `/crm/radar-log` | 访客雷达实时足迹 | 访客明细流水表(时间、国家城市 IP、触碰方式 NFC/QR、停留时长、滚动深度、浏览模块序列);深度访问高亮提示。 | `Table`, `Tag`, `Badge`, `Tooltip`, `Pagination` |
|
||||
| **线索管理** | `/crm/leads` | 潜客线索关系库 | 客户线索全量列表;意向度等级打标 (`Hot` / `Warm` / `Cold`);跟进人分配;批量导出 Excel;一键推送到外部 CRM。 | `Table`, `Select`, `Tag`, `Modal`, `Drawer`, `Message` |
|
||||
| **商机看板** | `/crm/pipeline` | 销售商机流转看板 | 泳道式阶段看板(新留资 ➔ 首次接触 ➔ 方案对接 ➔ 赢单/输单);卡片拖拽流转;商机总金额与转化率统计。 | `Grid`, `Card`, `Avatar`, `Tag`, `Typography.Text` |
|
||||
@@ -214,7 +214,7 @@ user-client/ (uni-app x 目录架构)
|
||||
│ │ │ ├── radar/ # 移动访客雷达
|
||||
│ │ │ ├── leads/ # 移动 CRM 与商机跟进
|
||||
│ │ │ ├── nfc/ # 手机 NFC 贴卡激活与场景管理
|
||||
│ │ │ └── profile/ # 个人设置与出海展会战报
|
||||
│ │ │ └── profile/ # 个人设置与展会活动战报
|
||||
│ │ └── common/ # 公共认证、404 与协议页面
|
||||
```
|
||||
|
||||
@@ -223,20 +223,20 @@ user-client/ (uni-app x 目录架构)
|
||||
| 端分类 | 页面路由路径 | 页面中文名称 | 核心功能点与交互规范 | 推荐复用原生/uni-ui组件 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **访客端** | `pages/portal/index` | **Bento 移动微官网主页** | **首屏秒开 (<500ms)**;自适应国际化与阿语 RTL;<br>1. **Hero 身份区**:头像、姓名、头衔、公司、时区、名字语音发音条;<br>2. **快捷动作条**:一键呼叫 WhatsApp / LinkedIn / 电话;<br>3. **常驻悬浮 CTA**:一键保存到手机通讯录 (vCard 3.0/4.0);<br>4. **Bento 瀑布流**:精选案例展厅、深度文章、服务定价卡;<br>5. **右下角悬浮**:24/7 AI 对话顾问唤起气泡。 | `<view>`, `<scroll-view>`, `<image>`, `<text>`, `uni-popup`, `uni-badge` |
|
||||
| **访客端** | `pages/portal/case-detail` | 案例作品集详情页 | 出海案例深度展示;多图画廊轮播;方案成效指标数据卡 (KPI Cards);嵌入式 PDF 白皮书在线预览;一键咨询按钮。 | `<swiper>`, `<image>`, `<rich-text>`, `<button>`, `uni-icons` |
|
||||
| **访客端** | `pages/portal/case-detail` | 案例作品集详情页 | 标杆案例深度展示;多图画廊轮播;方案成效指标数据卡 (KPI Cards);嵌入式 PDF 白皮书在线预览;一键咨询按钮。 | `<swiper>`, `<image>`, `<rich-text>`, `<button>`, `uni-icons` |
|
||||
| **访客端** | `pages/portal/article-detail`| 深度洞见与文章页 | 移动优化杂志级排版;沉浸式无干扰长文阅读;一键分享至全球社交平台(生成 OpenGraph 网页元标签)。 | `<rich-text>`, `<image>`, `<text>`, `uni-icons` |
|
||||
| **访客端** | `pages/portal/ai-chat` | **名片内置 AI 顾问** | 底部抽屉展开;专属名片企业/个人知识库 RAG 问答;多语言输入与语音朗读;智能意向识别;自动弹出留资卡片。 | `<list-view>`, `<list-item>`, `<input>`, `uni-popup`, `<button>` |
|
||||
| **访客端** | `pages/portal/exchange` | **无摩擦互换名片弹窗** | 极简留资表单(姓名、企业、WhatsApp/Email、需求留言);支持拍照/相册上传海外纸质名片(OCR 自动解析填入)。 | `uni-popup`, `uni-forms`, `uni-easyinput`, `<button>` |
|
||||
| **访客端** | `pages/portal/exchange` | **无摩擦互换名片弹窗** | 极简留资表单(姓名、企业、WhatsApp/Email、需求留言);支持拍照/相册上传纸质名片(OCR 自动解析填入)。 | `uni-popup`, `uni-forms`, `uni-easyinput`, `<button>` |
|
||||
| **名片主** | `pages/owner/card/index` | 移动名片管理中心 | 多张名片水平滑动卡片盒;名片状态标签(已发布/展会中/草稿);今日浏览数与未读雷达红点;快速分享海报入口。 | `<swiper>`, `<view>`, `uni-badge`, `uni-card`, `<button>` |
|
||||
| **名片主** | `pages/owner/card/edit` | 名片极速微调编辑 | 移动端随时更新个人资料、职位、手机号、社交链接;Bento 模块上下排序拖拽;内置 AI 文案一键多语言润色。 | `uni-forms`, `uni-easyinput`, `<switch>`, `<button>`, `uni-popup` |
|
||||
| **名片主** | `pages/owner/card/qrcode` | 出海参展分享码海报 | 动态高清二维码生成;展会易拉宝大图模式/手机锁屏壁纸名片生成;一键保存至手机相册或隔空投送。 | `<canvas>`, `<image>`, `<button>`, `uni-icons` |
|
||||
| **名片主** | `pages/owner/card/qrcode` | 参展与分享码海报 | 动态高清二维码生成;展会易拉宝大图模式/手机锁屏壁纸名片生成;一键保存至手机相册或隔空投送。 | `<canvas>`, `<image>`, `<button>`, `uni-icons` |
|
||||
| **名片主** | `pages/owner/radar/index` | **移动实时访客雷达** | **原生 60fps 高性能长列表**;实时访客足迹通知流(“迪拜客户正在浏览您的名片”);足迹明细与停留时长;高意向客户标红。 | `<list-view>`, `<list-item>`, `uni-badge`, `uni-tag` |
|
||||
| **名片主** | `pages/owner/leads/index` | 移动 CRM 潜客列表 | 潜客列表按意向度分类(全部 / `Hot` / `Warm` / `Cold`);一键唤起手机电话或跳转 WhatsApp 聊天;多维度关键词搜索。 | `uni-search-bar`, `<list-view>`, `uni-segmented-control`, `uni-card` |
|
||||
| **名片主** | `pages/owner/leads/detail` | 潜客详情与跟进备忘 | 客户全轨迹时间线(第几次扫码、浏览了哪些案例);AI 自动生成的破冰跟进话术;快捷添加跟进记录备忘。 | `<scroll-view>`, `uni-card`, `<textarea>`, `<button>`, `uni-icons` |
|
||||
| **名片主** | `pages/owner/nfc/scan` | **NFC 贴卡激活/写入** | 原生调起手机硬件 NFC 天线;贴近实体卡 0.5 秒写入短链与动态防伪鉴权码;配对成功微震动与全息波纹动效。 | `<view>`, `<image>`, `<button>`, 条件编译 `#ifdef APP` |
|
||||
| **名片主** | `pages/owner/nfc/mode` | 硬件场景模式切换 | 实体卡绑定名片快速切换(日间展会模式 ➔ 晚宴社交模式);离线 vCard 芯片烧录功能;卡片状态查看。 | `uni-card`, `uni-data-checkbox`, `<button>` |
|
||||
| **名片主** | `pages/owner/nfc/security`| NFC 安全与挂失 | 实体卡远程一键挂失冻结;卡片防伪 UID 校验;解除绑定与旧卡注销。 | `uni-card`, `uni-popup-dialog`, `<button>` |
|
||||
| **名片主** | `pages/owner/profile/index`| 个人账户与出海设置 | 个人信息与多语言切换;消息推送通知开关(雷达实时推送、线索即时提醒);所属企业与部门信息。 | `uni-list`, `uni-list-item`, `<switch>`, `<button>` |
|
||||
| **名片主** | `pages/owner/profile/index`| 个人账户与国际化设置 | 个人信息与多语言切换;消息推送通知开关(雷达实时推送、线索即时提醒);所属企业与部门信息。 | `uni-list`, `uni-list-item`, `<switch>`, `<button>` |
|
||||
| **名片主** | `pages/owner/team/overview`| 展会团队战报简报 | 展会期间团队获客排行榜;个人触达 KPI 与商机转化进度柱状图;优秀案例一键转存至个人名片。 | `uni-card`, `uni-badge`, `<scroll-view>` |
|
||||
| **公共层** | `pages/common/login` | 统一身份认证登录 | 国际手机号验证码登录、邮箱 Magic Link 免密登录、Google / Apple ID 一键快捷登录。 | `uni-forms`, `uni-easyinput`, `<button>` |
|
||||
|
||||
@@ -246,7 +246,7 @@ user-client/ (uni-app x 目录架构)
|
||||
|
||||
| 功能特权 | 免费版 (Free) | 专业版 (Pro) | 商业企业版 (Team/Enterprise) |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **适用人群** | 个人体验、初创独立人 | 出海业务精英、独立顾问 | 外贸销售团队、出海跨国企业 |
|
||||
| **适用人群** | 个人体验、初创独立人 | 全球业务精英、独立顾问 | 全球销售团队、跨国集团企业 |
|
||||
| **名片数量** | 1 张标准名片 | 5 张多语言/多场景名片 | 无限名片(按坐席计费) |
|
||||
| **微官网案例/文章** | 限制 2 个案例,无独立文章 | 无限案例,支持富媒体文章 | 统一品牌案例库,团队协同发布 |
|
||||
| **多语言支持** | 基础双语 (自动翻译) | 全球 10+ 语言 AI 深度润色 | 全球语言 + 企业专有术语库 |
|
||||
@@ -278,7 +278,7 @@ user-client/ (uni-app x 目录架构)
|
||||
- 接入大语言模型与向量知识库 (RAG),上线名片内嵌 24/7 AI 智能顾问浮层。
|
||||
- 落地实时访客雷达追踪(IP 地理定位、停留时长、意向度评分打标)。
|
||||
- 上线移动端雷达推送与 Arco 管理后台销售商机看板 (Pipeline)。
|
||||
4. **第四阶段:企业级多租户协作与商业化出海**
|
||||
4. **第四阶段:企业级多租户协作与全球商业化**
|
||||
- 上线企业品牌规范套件 (Brand Kit) 与离职人员资产无损交接工单。
|
||||
- 接入 Stripe 国际信用卡订阅支付体系与开票系统。
|
||||
- 开放 OpenAPI 与外部 CRM 生态(HubSpot、Salesforce)数据打通。
|
||||
|
||||
775
docs/mpuniapp-ui-refactor-plan.md
普通文件
775
docs/mpuniapp-ui-refactor-plan.md
普通文件
@@ -0,0 +1,775 @@
|
||||
# mpuniapp UI 重构规范
|
||||
|
||||
面向实现代理。只改 `.uvue` 模板结构、scoped CSS、以及 `resolveXxxText` 文案分支。禁止改业务 API、状态机、请求参数、路由语义。`pages.json` 的 tabBar 仍是旧「官网/雷达/潜客」,本轮只在分析中指出,不改路由配置。
|
||||
|
||||
Token 一律来自 `mpuniapp/common/tokens.uts`,页面通过已有 `pageTheme` 注入 CSS 变量。禁止 Tailwind。禁止业务态内联 `style`(`pageTheme` 注入 CSS 变量除外)。暗色变量已定义,本轮先把 Light 变量用齐;能接 `prefers-color-scheme` 的页面在第 5 轮统一补。
|
||||
|
||||
---
|
||||
|
||||
## 0. 全局视觉语言(实现代理必遵)
|
||||
|
||||
1. **页面骨架**:`背景 surface #F7F8FA` + 顶栏白底(`bg #FFFFFF` + 底部分隔 `1px border #E3E7ED`)+ 内容区 `padding-inline: 16px` + 列表/表单卡片 + 底栏 1 个主 CTA。页面纵向:顶栏固定内容高度,中间 `flex:1` 滚动,底栏不随内容滚走。
|
||||
2. **字号层次(只许 12/14/16/20/24/32/40)**:页标题 24/700/行高 1.25/`text`;区块标题 16/600;正文 14/行高 1.6/`text2`;辅助/标签 12/`text2`。禁止 11、13、15、17、18、22。按钮字 16/600。输入字 16。
|
||||
3. **卡片**:白底、`1px border`、圆角 16、阴影只许 `0 1px 2px rgba(16,24,40,0.04)`。最多两层嵌套。列表行用卡片,不用重阴影、不用玻璃、不用渐变底。
|
||||
4. **主 CTA 栏**:每屏 1 个主按钮。高 48、圆角 12、宽 100%、`primary #0B5CFF`、字 `#FFFFFF`。固定底栏:`padding 12px 16px` + `safe-area-inset-bottom`。内容区底部垫高 `88px + window-bottom + safe-area`。禁用态底 `border`、字 `text2`。次操作只能是文字链或描边 chip,不能再出现同级实心蓝按钮。
|
||||
5. **空 / 错 / 加载**:加载用骨架(头像圆 72、标题条 24×60%、卡片条 72–88 高、底色 `border`、圆角 8/16),禁止转圈作为主反馈。空态:一句话 16/600 + 一句 14/`text2` + 一个主 CTA。错误态:同卡片结构,主 CTA = 重试/返回。
|
||||
6. **分段 chips**:高 ≥44、左右 padding 16、圆角 12、默认白底+`border`、选中 `primarySoft` 底 + `primary` 边 + `primary` 字。横滑一行,禁止换行堆成两排。禁止选中填实心蓝底白字(语言选择除外可用描边选中)。
|
||||
7. **安全区与触控**:可点区域 ≥44×44。输入高 44、标签在上、错误在下。删除徽标若视觉 18,外扩点击热区到 44。底栏避让 `env(safe-area-inset-bottom)` 与 `var(--window-bottom)`(有 tabBar 的页必须加 window-bottom)。
|
||||
8. **RTL**:间距、padding、margin 用 logical(`padding-inline` / `margin-inline` / `border-inline-start`)。箭头、悬浮入口在 RTL 下左右对调。`direction` 跟当前 locale。
|
||||
9. **动效**:点击透明度 0.85 或位移 0,时长 150ms,`cubic-bezier(0.2, 0.8, 0.2, 1)`。禁止 scale 放大。`prefers-reduced-motion: reduce` 时 animation/transition 全关。AI 只用 150ms 微光一次。NFC 脉冲 400ms 一次,不循环。
|
||||
10. **禁止**:emoji、纯黑 `#000`、大面积渐变、重阴影、玻璃拟态、霓虹、拟物、装饰插画。品牌色只作按钮/链接/选中,语义红绿黄只表示状态。模板禁止硬编码中文,走 `getOmniText` / `resolveXxxText`。
|
||||
11. **长列表**:`list-view` + `list-item`。禁止 `view` + `v-for` 渲染可能超过一屏的列表。Bento 网格、KPI(≤4)、语言 chips(7)、状态 chips(≤6)可用 `view` + `v-for`。
|
||||
12. **不要改的范围**:`common/api.uts`、`common/request.uts`、`common/types.uts`、各页 `useXxx.uts` 的请求/状态机/跳转 URL。允许:`.uvue` 结构与 CSS;`resolveXxxText` 增补文案 key(不改函数签名语义)。本轮不改 `pages.json` tabBar。
|
||||
|
||||
---
|
||||
|
||||
## 1. 页面总表
|
||||
|
||||
| 路径 | 中文名 | 角色 | PRD 必须内容 | 当前缺口 | UX 改进点 | 重构优先级 |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| `pages/portal/index` | Bento 微官网 | 访客首屏 | Hero、快捷动作、vCard 主 CTA、Bento、AI 入口、i18n/RTL | 顶栏三动作抢主 CTA;AI 浮层阴影过重;社交格全显空项;大量硬编码色;`collect` 文案写死 | 压顶栏、滤空社交、字号归位、AI 降为次入口 | P0 |
|
||||
| `pages/portal/exchange` | 互换名片 | 访客留资 | 姓名/公司/WhatsApp/邮箱/留言;OCR 拍照填单 | 模板全中文硬编码;OCR 按钮高 36;无 RTL | 全走 i18n;表单标签上/错下;OCR 热区 44 | P0 |
|
||||
| `pages/portal/case-detail` | 案例详情 | 访客阅读 | 轮播、KPI、PDF 预览、咨询 CTA | `CASE_DETAIL_COPY` 中文写死;PDF/视频只露 URL | 文案走 resolve;外链做成可点行;KPI 最多 3 张 | P0 |
|
||||
| `pages/portal/article-detail` | 文章详情 | 访客阅读 | 杂志排版、分享 | 底栏「复制链接」语义弱;空态无卡片感 | 主 CTA 改「复制分享链接」层次;阅读列宽保持 720 | P1 |
|
||||
| `pages/portal/ai-chat` | AI 顾问 | 访客问答/留资 | list-view 对话、留资卡、发送 | 输入+发送纵向占太高;成功色硬编码 | 输入行与发送合并为一行;微光保留 | P1 |
|
||||
| `pages/common/login` | 登录 | 名片主认证 | 手机验证码、Magic Link、Google/Apple | 模板中文写死;无 token 变量;PRD 三通道未上屏;测试账号暴露 | 先视觉+i18n;测试入口降为 12 号弱链;预留 tab 位但不接新 API | P0 |
|
||||
| `pages/owner/hall/index` | 名片大厅 | Tab「大厅」 | 人脉/团队/现场;搜;兑换码/现场码 | 顶栏过高;空态无 CTA;列表无头像位 | 压标题;三段 chip 单行;空态接底栏主 CTA | P1 |
|
||||
| `pages/owner/card/index` | 我的名片 | Tab「名片」 | 卡片盒、状态、今日浏览、雷达红点、海报 | 普通列表;无浏览数/红点;底栏「刷新/海报」语义飘 | 行内状态+编辑;底栏固定「分享海报」;浏览数可占位不接新接口 | P0 |
|
||||
| `pages/owner/radar` | 实时雷达 | 二级页 | 长列表足迹、停留、高意向标红 | 手填 cardId 过技术;空/错混为一句;无骨架;不高亮 | 去掉技术输入视觉(保留字段隐藏绑定);行内标红高停留 | P1 |
|
||||
| `pages/owner/leads` | 客户 | Tab「客户」 | 意向筛选、搜索、电话/WhatsApp;高意向红点;雷达摘要 | 顶栏堆 6 块;红点用了品牌蓝;无一键联系 | 压成「标题+红点 / 雷达 2 行 / 搜索 / chips」;红点用 error | P0 |
|
||||
| `pages/owner/profile/index` | 我的 | Tab「我的」 | 资料、语言、推送、企业 | 硬编码色;进度条内联 width;预览钮 32;删除热区 18;指标写死 | 改 token;热区 44;语言 chip 改描边选中 | P1 |
|
||||
| `pages/owner/card/edit` | 极速编辑 | 二级 | 资料、社交、模块排序、AI 润色 | 无 slogan/排序/润色入口;删除热区 18 | 只补视觉空位与层次,不接新 API | P1 |
|
||||
| `pages/owner/card/qrcode` | 分享海报 | 二级 | 高清码、大图、存相册 | 只有 URL 文本,无码无海报 | 中央 200 方码位+短链;主 CTA「复制链接」 | P0 |
|
||||
| `pages/owner/leads-detail` | 跟进备忘 | 二级 | 轨迹、AI 话术、备忘 | 轨迹用 view 列表;无电话/WhatsApp;无 AI 话术位 | 轨迹改 list-view;预留话术卡;主 CTA 仍保存备忘 | P1 |
|
||||
| `pages/owner/nfc/scan` | NFC 贴卡 | 二级 | 贴卡 0.5s、脉冲、成功震动 | 纯表单;无脉冲;H5 与 App 长一样 | 中央 120 脉冲环(一次 400ms);表单降为 H5 后备 | P1 |
|
||||
| `pages/owner/nfc/mode` | 场景切换 | 二级 | 展会/社交、离线 vCard、状态 | 缺离线说明行 | 补一行说明;模式选项保持描边 | P2 |
|
||||
| `pages/owner/nfc/security` | 挂失 | 二级 | 挂失、UID 校验、解绑 | `view v-for`;「处理中...」写死;危险按钮 `#D92D20`;底 `#FFFFFF` | 设备少可保留卡片;CTA 用 error token;文案 i18n | P2 |
|
||||
| `pages/owner/team/overview` | 展会战报 | 二级 | 排行、KPI、转化柱 | 刷新当主 CTA;排行数字裸堆;双 list 分支重复 | 2×2 KPI 用 gap 12;主 CTA 改「回到客户」或保留刷新但降文案 | P2 |
|
||||
|
||||
**tabBar 现状(本轮不改文件)**:`pages.json` 仍为 官网=`portal/index` / 名片 / 雷达 / 潜客=`leads` / 我的。落地 IA 应为 大厅 / 名片 / 客户 / 我的,雷达从客户页进入。视觉重构按落地 IA 排信息,不改 `pages.json`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 逐页规范
|
||||
|
||||
### 2.1 `pages/portal/index` · Bento 移动微官网
|
||||
|
||||
**用户目标**:5 秒内确认对方是谁,并保存到通讯录。
|
||||
|
||||
**必须保留**:`loadCardPortal`、停留上报、语言 persist、快捷动作、vCard 保存、Bento 跳转、互换/AI 跳转、收藏大厅、发音组件、骨架/错误重试。
|
||||
|
||||
**信息架构(上→下)**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ [语言选择] [交换 文字] │ 高 44,无第二实心按钮
|
||||
├─────────────────────────────────────┤
|
||||
│ 品牌 Logo(可选) │
|
||||
│ ┌ Hero 卡 16 圆角 1px 边 ─────────┐ │
|
||||
│ │ 头像 72 时区 pill 12 │ │
|
||||
│ │ 姓名 24/700 [发音] │ │
|
||||
│ │ 头衔 14 / 公司 14/600 │ │
|
||||
│ │ 分隔线 + Slogan 14/1.6 │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
│ 快捷动作:仅渲染有值的项,每项 25% │
|
||||
│ 宽、高 ≥56,字 12 │
|
||||
│ Bento:1 列 50% / 2 列 100%,gap 12 │
|
||||
│ spacer 140+safe │
|
||||
├─────────────────────────────────────┤
|
||||
│ 主 CTA 48:「保存到通讯录」 │
|
||||
└─────────────────────────────────────┘
|
||||
右下 AI 文字 chip(次入口,不是主 CTA)
|
||||
```
|
||||
|
||||
**主 CTA**:底栏「保存到通讯录」`card.save_contact`。AI、交换、收藏都是文字/描边。
|
||||
|
||||
**空 / 错 / 骨架**:保持现有三态。错误卡居中,重试按钮高 40 改为 44。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- 顶栏去掉「收入大厅」与「交换」并列双 chip。交换保留一个 14 号文字链。收藏改为 Hero 内 12 号文字链,或登录后才显示。
|
||||
- `collectLabel`、toast「已收入大厅」改为 `resolve` / locale key,禁止 `currentLang === 'zh-CN' ?`。
|
||||
- 快捷动作:无链接的项不渲染(数据仍在 state,只是 `v-if`)。
|
||||
- 字号:`action-icon-text` 11→12,`hero-title` 15→14,`hero-slogan` 13→14,`bento-title` 15→16,`bento-tag` 11→12,CTA 字 15→16。
|
||||
- 颜色全部改 `var(--oc-*)`。页面背景 `surface`。
|
||||
- `.ai-entry` 阴影改为 token 浅影,去掉紫影 `rgba(124,92,255,0.12)`。
|
||||
- `.bento-card:active` 的 `scale(0.99)` 改为 `opacity: 0.85`,并加 `prefers-reduced-motion`。
|
||||
- RTL:顶栏、姓名行、公司行改 `margin-inline`;AI 入口 `inset-inline-end: 16px`。
|
||||
- 公司行保持可点,不改 `handleCompanyClick`。
|
||||
|
||||
**不要改的文件**:`usePortal.uts`、`useAudioPlayer.uts`、`useCache.uts`、`vcard-export.uts`、`components/i18n-selector`、`components/audio-pronounce`。
|
||||
|
||||
---
|
||||
|
||||
### 2.2 `pages/portal/exchange` · 无摩擦互换名片
|
||||
|
||||
**用户目标**:30 秒留下姓名和一个联系方式。
|
||||
|
||||
**必须保留**:`applyExchangeOptions`、`submitExchange`、OCR 上传/预览/删除、`canSubmit`/`submitting` 禁用。
|
||||
|
||||
**信息架构**
|
||||
|
||||
```
|
||||
┌ 白卡 16 圆角 ──────────────────────┐
|
||||
│ 标题 24:「交换名片」 │
|
||||
│ 说明 14 │
|
||||
│ 警告条(无 cardId 时) │
|
||||
│ OCR 行:左文案 / 右 44 高描边按钮 │
|
||||
│ 预览行:48 图 + 文 + 删除(热区 44) │
|
||||
│ 字段×5:标签 14 在上,输入 44 │
|
||||
└─────────────────────────────────────┘
|
||||
底栏主 CTA 48:提交 / 提交中
|
||||
```
|
||||
|
||||
**主 CTA**:提交留资。OCR 是次按钮(`primarySoft` 底 + `primary` 边)。
|
||||
|
||||
**空 / 错 / 骨架**:无 cardId 用警告条,主 CTA 禁用。无独立骨架(表单即首屏)。提交失败走现有 toast,不改 hook。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- 模板 10+ 处中文全部改为 `resolveExchangeText`。若 hook 无该函数,只在 `useExchange.uts` **新增文案 map**,不改 `submitExchange`。
|
||||
- `.ocr-btn` 高 36→44。`.ocr-preview-delete` padding 扩到 min 44×44。
|
||||
- 输入已是 44,保持。标签 14/600,间距 8。
|
||||
- 页面加 `:class` RTL。`margin-right` 改 `margin-inline-end`。
|
||||
- CTA 字 15→16。
|
||||
|
||||
**不要改的文件**:`useOcrExchange.uts` 的上传/识别流程;`submitExchange` 入参。
|
||||
|
||||
---
|
||||
|
||||
### 2.3 `pages/portal/case-detail` · 案例详情
|
||||
|
||||
**用户目标**:看懂成果,点「咨询」去留资。
|
||||
|
||||
**必须保留**:`loadCaseDetail`、swiper、KPI、pdf/youtube 字段、`openCaseExchange`、空态返回。
|
||||
|
||||
**信息架构**
|
||||
|
||||
```
|
||||
标题 24
|
||||
封面 swiper 220 / 单图 220,圆角 16
|
||||
正文 16 / 行高 1.6
|
||||
KPI 行:最多 3 张,每张 value 24 + label 12
|
||||
外链行:左标签 12 / 右可点 URL 14 primary(看起来像行按钮,不是裸字符串)
|
||||
spacer
|
||||
底栏:「咨询」→ 现有 openCaseExchange
|
||||
```
|
||||
|
||||
**主 CTA**:咨询。空态主 CTA = 返回。
|
||||
|
||||
**空 / 错 / 骨架**:保持。空态卡片加 `max-width` 不拉满刺眼。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- `CASE_DETAIL_COPY` 从模板剥离:模板改调 `resolveCaseDetailText`(可在 hook 把现有对象包一层函数)。禁止模板直接绑中文字面量。
|
||||
- 外链行加 `min-height: 44`,整行可点打开/复制(若 hook 无 open,则只复制,不新写请求)。
|
||||
- KPI `margin-inline-end` 改为 `gap: 8`,最后一张不要多余右边距。
|
||||
- 无暗色/无 RTL class:补 `padding-inline` 与页面 rtl class。
|
||||
|
||||
**不要改的文件**:`loadCaseDetail` 解析逻辑。
|
||||
|
||||
---
|
||||
|
||||
### 2.4 `pages/portal/article-detail` · 文章
|
||||
|
||||
**用户目标**:不被打断地读完,再决定分享。
|
||||
|
||||
**必须保留**:`loadArticleDetail`、rich-text、复制分享、空态返回。
|
||||
|
||||
**信息架构**
|
||||
|
||||
```
|
||||
封面 200 圆角 16(可无)
|
||||
kicker 12/600 primary
|
||||
标题 24
|
||||
正文 16 / 行高 1.7,容器 max-width 720
|
||||
spacer
|
||||
底栏主 CTA:复制分享链接
|
||||
```
|
||||
|
||||
**主 CTA**:复制链接(现有 `copyArticleShareLink`)。不要再加第二个分享按钮。
|
||||
|
||||
**空 / 错 / 骨架**:空态改与案例一致的白卡片+一句话+返回,不要只有灰字贴边。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- 空态加白底边框卡、居中。
|
||||
- CTA 字 15→16。
|
||||
- 保持杂志留白,不要把文章塞进卡片框。
|
||||
|
||||
**不要改的文件**:`useArticleDetail.uts`。
|
||||
|
||||
---
|
||||
|
||||
### 2.5 `pages/portal/ai-chat` · AI 顾问
|
||||
|
||||
**用户目标**:问一句、得到答复;有意向时留下姓名+联系方式。
|
||||
|
||||
**必须保留**:`sendStreamChat`、`submitLeadCard`、`list-view` 消息、留资卡状态机、缺 cardId 提示。
|
||||
|
||||
**信息架构**
|
||||
|
||||
```
|
||||
┌ 圆角 20 顶 sheet ───────────────────┐
|
||||
│ 把手 40×4(ai 色 20% 透明) │
|
||||
│ 标题 24 │
|
||||
│ list-view:空态一句 / 气泡 / 留资卡 │
|
||||
│ 底作曲:标签 12 + [输入 44 | 发送 48]│
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**主 CTA**:发送。留资卡内提交是卡片内次按钮(描边),不算第二屏主 CTA。
|
||||
|
||||
**空 / 错 / 骨架**:空态保持一句。发送中按钮禁用文案走 `sending`。无骨架(对话页)。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- 作曲区改为横向:`input flex:1` + 发送宽不固定、高 48,避免键盘顶起三行。若 uni-app x 横向不稳,保持纵向但去掉重复「输入标签」,只留 placeholder。
|
||||
- 成功态绿底 `#ECFDF3` 改为 `surface` + 左边框 `success`,避免大块绿。
|
||||
- `.ai-lead-error`、硬编码 `#12B76A`/`#F04438` 改 token 变量。
|
||||
- 微光 animation 保留,`prefers-reduced-motion` 已有,保持。
|
||||
- 用户气泡白字对比度足够,保留。
|
||||
|
||||
**不要改的文件**:`useAiChat.uts` 流式与留资提交。
|
||||
|
||||
---
|
||||
|
||||
### 2.6 `pages/common/login` · 登录
|
||||
|
||||
**用户目标**:用已有账号进入名片主。
|
||||
|
||||
**必须保留**:`submitPasswordLogin`、`fillTestAccount`、条款 toast、登录后 pending 收藏。
|
||||
|
||||
**信息架构**
|
||||
|
||||
```
|
||||
居中列 max-width 340
|
||||
Logo 36
|
||||
标题 24(现 22 改 24)
|
||||
说明 14
|
||||
输入 用户名 44
|
||||
输入 密码 44
|
||||
弱文字链 12:测试账号(不要做成按钮)
|
||||
错误 12 error 色
|
||||
主 CTA 48:登录 / 登录中
|
||||
底部条款 12 text2 + 链接
|
||||
```
|
||||
|
||||
**主 CTA**:登录。不要同时放验证码/Google 实心按钮。PRD 的手机/邮箱/社交本轮只允许在标题下加 12 号「更多方式即将开放」一句,不接新 API。
|
||||
|
||||
**空 / 错 / 骨架**:错误在按钮上方。无骨架。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- 模板中文全部改 `resolveLoginText`(hook 已有 key:`title`/`desc`/`usernamePlaceholder`/`passwordPlaceholder`/`testAccountTip`/`login`/`terms*`)。
|
||||
- 引入 `pageTheme` + `var(--oc-*)`。圆角 10→12。按钮高 44→48。
|
||||
- 测试账号从高饱和蓝链改为 `text2`,点击仍 `fillTestAccount`。
|
||||
- 页面背景 `bg`。补 RTL class。
|
||||
- `aria-label` 走 i18n,禁止写死「登录」。
|
||||
|
||||
**不要改的文件**:`submitPasswordLogin`、token 写入、`consumePendingCollectCard`。
|
||||
|
||||
---
|
||||
|
||||
### 2.7 `pages/owner/hall/index` · 大厅(人脉 / 团队 / 现场)
|
||||
|
||||
**用户目标**:找到已收的人,或用码加入。
|
||||
|
||||
**必须保留**:三 tab、`refreshHall`、兑换码、现场码、`openHallPortal`、list-view。
|
||||
|
||||
**信息架构**
|
||||
|
||||
```
|
||||
白顶栏:
|
||||
标题 24
|
||||
副标题 14(只 1 行,过长截断)
|
||||
chips ×3 单行
|
||||
搜索 44
|
||||
条件第二输入 44(人脉=兑换码 / 现场=现场码)
|
||||
list-view 行卡:
|
||||
左 40 圆占位(无头像字段则纯色块,不请求新字段)
|
||||
右:姓名 16 / 头衔 14 / 公司 14
|
||||
空/错卡片
|
||||
底栏主 CTA:按现有 ctaLabel(刷新 / 兑换 / 加入现场)
|
||||
```
|
||||
|
||||
**主 CTA**:沿用现有三态文案,不要拆成两个底按钮。
|
||||
|
||||
**空 / 错 / 骨架**:空态必须能被底栏 CTA 服务(刷新/加入)。空态卡片不要再放第二个按钮。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- 顶栏 `padding-block-end` 保持 12,tab 与搜索间距 8,去掉多余 12+12 堆叠。
|
||||
- 行卡加 8px 间隔,左占位 40×40 圆角 20,`surface` 底。不改数据类型。
|
||||
- 错误态补「可点热区」靠底栏重试,不要在卡片里再做按钮。
|
||||
|
||||
**不要改的文件**:`useHall.uts`。
|
||||
|
||||
---
|
||||
|
||||
### 2.8 `pages/owner/card/index` · 我的名片
|
||||
|
||||
**用户目标**:看自己有哪些名片,去预览、编辑或分享。
|
||||
|
||||
**必须保留**:搜索、`refreshCards`、预览、编辑、`pickShareCard`+`openCardQrcode`、配额 subtitle。
|
||||
|
||||
**信息架构**
|
||||
|
||||
```
|
||||
白顶栏:标题 24 / 配额句 14 / 搜索 44
|
||||
list-view 行:
|
||||
左:姓名 16 / 职位 14 / 公司 14
|
||||
右:状态 12 primary
|
||||
今日浏览 12 text2(无字段则显示 "—",不造假数、不改 API)
|
||||
「编辑」文字热区 44
|
||||
底栏主 CTA:有可分享名片 → 分享海报;否则刷新
|
||||
```
|
||||
|
||||
**主 CTA**:保持现有 `handleRefresh` 分支(有卡则海报,无则刷新)。文案在有卡时应显示「分享海报」而不是「刷新」。实现:模板按 `pickShareCard(state.list) != null` 切换已有 `resolveCardHomeText`,不改函数。
|
||||
|
||||
**空 / 错 / 骨架**:空态一句话说明「还没有名片」,主 CTA 仍刷新。不要做「新建」除非已有 API。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- 行高 min 88 保持。状态不要和编辑抢成两个蓝按钮,编辑保持文字。
|
||||
- PRD 的横向 swiper 本轮不做(会动结构),用列表+清晰状态代替。
|
||||
- 补 `prefers-reduced-motion` 去掉行 `scale`。
|
||||
|
||||
**不要改的文件**:`useCardHome.uts`、`fetchCardQuota`。
|
||||
|
||||
---
|
||||
|
||||
### 2.9 `pages/owner/radar` · 实时雷达(二级)
|
||||
|
||||
**用户目标**:看到谁在看名片、看了多久。
|
||||
|
||||
**必须保留**:`refreshTracks`、`cardIdRaw` 绑定、list-view、停留/深度格式化。
|
||||
|
||||
**信息架构**
|
||||
|
||||
```
|
||||
标题 24 + 说明 14
|
||||
(cardId 输入默认隐藏;仅当 cardId 为空时显示,避免销售看到「填 ID」)
|
||||
list-view:
|
||||
地点 16/600
|
||||
触达方式 12 标签
|
||||
停留 + 深度 14
|
||||
停留 ≥180s 或深度 ≥0.6:左边框 3px error(高意向,不改数据)
|
||||
空态一句
|
||||
底栏:刷新
|
||||
```
|
||||
|
||||
**主 CTA**:刷新。
|
||||
|
||||
**空 / 错 / 骨架**:错与空拆开:`errorMessage` 用错误卡;`empty` 用空态句。补与大厅同款骨架(现无)。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- 列表底垫高加上 `window-bottom`(若从 tab 进)。
|
||||
- `touchType` 原始值可显示,但用 12 号 `text2`,不要当标题。
|
||||
- 无时间字段则不加。不请求新 API。
|
||||
|
||||
**不要改的文件**:`useRadar.uts` 请求。
|
||||
|
||||
---
|
||||
|
||||
### 2.10 `pages/owner/leads` · 客户
|
||||
|
||||
**用户目标**:按意向找到人,点进去跟进;必要时看雷达。
|
||||
|
||||
**必须保留**:状态 chips、搜索、`refreshLeads`、`hotUnread`、`radarTracks` 三行、跳转详情/雷达/战报、list-view。
|
||||
|
||||
**信息架构(压缩顶栏)**
|
||||
|
||||
```
|
||||
标题 24 [红点 44:高意向 N] ← 点击仍 openRadar
|
||||
雷达摘要卡(最多 2 行 + 「全部雷达」文字)
|
||||
搜索 44
|
||||
chips 单行横滑:全部/新/高意向/已联系/赢单/归档
|
||||
list-view:姓名 16 / 公司 14 │ 分 12 + 状态 12
|
||||
底栏:搜索/刷新(现有 cta)
|
||||
```
|
||||
|
||||
**主 CTA**:底栏搜索刷新。红点、雷达、战报都是文字/描边。战报入口从顶栏挪走:放到雷达卡最后一行右侧,或本轮藏进雷达页/我的(数据函数保留,入口可移到「我的」已有战报行)。**本页只保留一个「战报」文字链,放雷达卡内,不再单独占一行大入口。**
|
||||
|
||||
**空 / 错 / 骨架**:保持。空态不要第二个按钮。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- `.radar-hot-badge`:底改极浅红(`error` 8% 不够就用白底+`error` 字+`error` 边),禁止 `primarySoft`。有未读才渲染。
|
||||
- chips:去掉 `flex-wrap` 与 `margin-block-end`,只横滑。
|
||||
- 行卡右侧状态:`hot` 用 `error` 字,其它用 `primary`。
|
||||
- 不做电话/WhatsApp 图标(列表无号码字段则不编造)。进详情再联系。
|
||||
- 顶栏总高目标:红点与标题同一行后,首屏至少露出 2 条客户。
|
||||
|
||||
**不要改的文件**:`useLeads.uts`、`openRadarPage`。
|
||||
|
||||
---
|
||||
|
||||
### 2.11 `pages/owner/profile/index` · 我的
|
||||
|
||||
**用户目标**:确认身份、改语言/推送、进 NFC 与战报、退出。
|
||||
|
||||
**必须保留**:头像选/删/预览、三项指标跳转、配额条、NFC 三入口、战报、两个 switch、语言 7 项、退出确认。
|
||||
|
||||
**信息架构**
|
||||
|
||||
```
|
||||
Hero 卡:头像 56 + 名 16 + 套餐 pill + 公司/部门 14
|
||||
分隔线 + 「预览微官网」文字链(热区 44,不再用 32 高按钮)
|
||||
指标 3 格:数字 24 / 标签 12
|
||||
配额卡:标题 + 用量 + 轨道 4 高 + 升级文字链
|
||||
工作区列表(分隔线,不要每行一张卡)
|
||||
通知 switch 行(热区 44)
|
||||
语言 chips 描边选中(不要实心蓝底白字)
|
||||
退出:text2 文字,居中
|
||||
错误条
|
||||
spacer(含 window-bottom)
|
||||
```
|
||||
|
||||
**主 CTA**:本页无底栏主 CTA(设置页)。「预览微官网」是唯一强调文字链。退出不是主 CTA。
|
||||
|
||||
**空 / 错 / 骨架**:错误条移到 Hero 下,不要沉底。无骨架(本地设置)。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- 全部硬编码色改 token。加 `pageTheme`。
|
||||
- 进度条:禁止内联 `:style width`。改用 10 档 class(`w-10`…`w-100`)或 `transform: scaleX(n)` 写在 class;`calculateQuotaPercent` 可映射到 class 名,不改计算函数。
|
||||
- 删除徽标视觉 18,外层点击层 44×44。
|
||||
- `.preview-btn` 高 32→44,或改纯文字链。
|
||||
- 语言选中:`primarySoft` + `primary` 边 + `primary` 字,取消白字填蓝。
|
||||
- 指标数字若仍是占位(12/5),保持,不编新接口。
|
||||
- 补 RTL logical。
|
||||
|
||||
**不要改的文件**:`useProfile.uts` 的开关/语言/退出/配额请求。
|
||||
|
||||
---
|
||||
|
||||
### 2.12 `pages/owner/card/edit` · 极速编辑
|
||||
|
||||
**用户目标**:改姓名/职位/电话/社交并保存。
|
||||
|
||||
**必须保留**:头像选/预览/删、各 input、`submitCardEdit`、缺 id 空态。
|
||||
|
||||
**信息架构**
|
||||
|
||||
```
|
||||
白表单卡:
|
||||
标题 24
|
||||
头像 80 + 上传/更换 44 文字按钮
|
||||
字段:姓名 / 职位 / 公司 / 电话 / 邮箱 / WhatsApp / LinkedIn
|
||||
(slogan、模块排序、AI 润色:本轮只在标题下加 12 号说明「更多编辑请用后台搭建器」,不接 API)
|
||||
错误 12 error
|
||||
底栏:保存
|
||||
```
|
||||
|
||||
**主 CTA**:保存。
|
||||
|
||||
**空 / 错 / 骨架**:保持。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- 删除热区扩到 44。头像边框 2px primary 改为 1px `border`(降低装饰)。
|
||||
- 错误色 `text2` → `error`。
|
||||
- CTA 字 16。字段间距 16。
|
||||
- 不要在本页做拖拽排序。
|
||||
|
||||
**不要改的文件**:`useCardEdit.uts`。
|
||||
|
||||
---
|
||||
|
||||
### 2.13 `pages/owner/card/qrcode` · 分享海报
|
||||
|
||||
**用户目标**:把码或链接给到对面。
|
||||
|
||||
**必须保留**:`loadPosterIntoState`、`copyPosterUrl`、空/错/骨架。
|
||||
|
||||
**信息架构**
|
||||
|
||||
```
|
||||
白卡居中:
|
||||
标题 24
|
||||
200×200 码位(有 svg 就渲染;无 svg 用 surface 底 + 12 号「码生成中/不可用」)
|
||||
短链 14,可断行
|
||||
就绪句 12 text2
|
||||
底栏:复制链接(现有 copyCta)
|
||||
```
|
||||
|
||||
**主 CTA**:复制链接。不新增「存相册」除非 `useCardQrcode` 已有该方法。
|
||||
|
||||
**空 / 错 / 骨架**:保持三态。空态不要假装有码。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- 有 `state.svg` 时用 `rich-text` 或 `image` 展示(哪种能跑用哪种,禁止引入新库)。
|
||||
- 码位边框 1px、圆角 16、居中。
|
||||
- 不要做易拉宝背景、不要大渐变海报。
|
||||
|
||||
**不要改的文件**:`useCardQrcode.uts`。
|
||||
|
||||
---
|
||||
|
||||
### 2.14 `pages/owner/leads-detail` · 跟进备忘
|
||||
|
||||
**用户目标**:看清这人是谁、来过几次,写下一条跟进。
|
||||
|
||||
**必须保留**:画像、轨迹、`nextNote`、图片上传、`submitLeadNote`。
|
||||
|
||||
**信息架构**
|
||||
|
||||
```
|
||||
画像卡:姓名 24 / 公司 14 / 分+状态
|
||||
(电话/邮箱:有字段才显示 14 文字,无则省略)
|
||||
轨迹:标题 16 + list-view(禁止 view v-for)
|
||||
AI 话术卡(无接口则静态一句 resolve 文案 + 灰字「即将提供」,不要假数据)
|
||||
备忘:已有笔记 14 + textarea min 88 + 上传
|
||||
底栏:保存备忘
|
||||
```
|
||||
|
||||
**主 CTA**:保存备忘。不要在底栏放 WhatsApp 实心按钮。
|
||||
|
||||
**空 / 错 / 骨架**:保持。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- `.lead-body` 底垫高改 `calc(88px + safe-area)`,现在写死 88 会贴 Home 条。
|
||||
- 轨迹改 `list-view`/`list-item`。短列表也统一,避免超过一屏时卡顿。
|
||||
- 状态 `hot` 用 error 字。
|
||||
- 上传区上方加 12 号说明(i18n)。
|
||||
|
||||
**不要改的文件**:`useLeadDetail.uts`、`image-uploader`。
|
||||
|
||||
---
|
||||
|
||||
### 2.15 `pages/owner/nfc/scan` · 贴卡激活
|
||||
|
||||
**用户目标**:把实体卡贴上并完成绑定。
|
||||
|
||||
**必须保留**:`startScan`(APP)、`submitScan`、uid/cardId 输入、状态行。
|
||||
|
||||
**信息架构**
|
||||
|
||||
```
|
||||
中央脉冲环 120(border 2px primary,opacity 脉冲 400ms 一次)
|
||||
标题 24 + 说明 14
|
||||
状态句 14
|
||||
H5 或失败时才显示 uid / cardId 两个字段
|
||||
底栏:激活
|
||||
```
|
||||
|
||||
**主 CTA**:激活/写入。
|
||||
|
||||
**空 / 错 / 骨架**:状态行即反馈。不要循环动画。`prefers-reduced-motion` 时只显示静止环。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- App:突出环,表单折叠。H5:`manualInput` 时显示表单(已有条件编译,只改样式权重)。
|
||||
- 禁止全息/多层光晕。一次脉冲:`opacity 0.4→1→0.4` 400ms,`animation-iteration-count: 1`。
|
||||
- 不改 `startScan`。
|
||||
|
||||
**不要改的文件**:`useScan.uts`。
|
||||
|
||||
---
|
||||
|
||||
### 2.16 `pages/owner/nfc/mode` · 场景切换
|
||||
|
||||
**用户目标**:把这张卡切到展会或社交。
|
||||
|
||||
**必须保留**:uid/cardId、状态/场景展示、`selectSceneMode`、`submitApplyScene`。
|
||||
|
||||
**信息架构**
|
||||
|
||||
```
|
||||
标题 24 + 说明 14
|
||||
两个字段
|
||||
只读状态行
|
||||
模式两选项(描边,选中 primarySoft)
|
||||
12 号说明:离线 vCard 由系统在弱网时写入(不接新按钮)
|
||||
底栏:应用
|
||||
```
|
||||
|
||||
**主 CTA**:应用场景。
|
||||
|
||||
**空 / 错 / 骨架**:无独立空态。提交中禁用。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- 选项不要做成两个主按钮。
|
||||
- 补 RTL。CTA 字 16。
|
||||
|
||||
**不要改的文件**:`useMode.uts`。
|
||||
|
||||
---
|
||||
|
||||
### 2.17 `pages/owner/nfc/security` · 安全与挂失
|
||||
|
||||
**用户目标**:选中一张卡,挂失或解冻。
|
||||
|
||||
**必须保留**:选中、`freezeDevice`/`unfreezeDevice`、UID 校验展示、警告卡。
|
||||
|
||||
**信息架构**
|
||||
|
||||
```
|
||||
标题 24 + 说明 14
|
||||
设备卡列表(当前 1 张,view 可接受;≥8 必须改 list-view)
|
||||
批次 12 / UID 16 等宽 / 状态 pill
|
||||
防伪点 + 文案 12
|
||||
警告卡(surface,不要黄底大块)
|
||||
底栏:挂失 | 解冻(随选中状态,仍是 1 个主按钮)
|
||||
```
|
||||
|
||||
**主 CTA**:挂失用 `error #F04438`(token),解冻用 `primary`。禁止 `#D92D20`。
|
||||
|
||||
**空 / 错 / 骨架**:空卡保持。`处理中...`、`操作异常` 改 `resolveSecurityText`。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- 页面背景改 `surface`,选中卡白底 + primary 边。
|
||||
- pill 圆角 12,不要 9999 胶囊霓虹感。
|
||||
- `#FFFFFF` / `#F8F9FB` / `#ECFDF3` 改 token 或 surface。
|
||||
- 底栏加 `window-bottom`(从「我的」进无 tab,可只 safe-area)。
|
||||
|
||||
**不要改的文件**:`useSecurity.uts` 校验与冻结。初始 mock 设备数组可留。
|
||||
|
||||
---
|
||||
|
||||
### 2.18 `pages/owner/team/overview` · 展会战报
|
||||
|
||||
**用户目标**:看团队谁在获客、自己差多少。
|
||||
|
||||
**必须保留**:`refreshOverview`、四指标、排行 list-view、最近线索 list-view。
|
||||
|
||||
**信息架构**
|
||||
|
||||
```
|
||||
标题 24 + 说明 14
|
||||
2×2 KPI,gap 12(不要 48%+4% hack)
|
||||
排行:名 16 / 「线索·触达·分」14 / 名次 12
|
||||
最近线索:名 / 公司 / 状态(状态走已有 label,不要裸 new/hot)
|
||||
底栏:刷新(或文案改为「更新战报」)
|
||||
```
|
||||
|
||||
**主 CTA**:刷新。不要加「转存案例」按钮(无 API)。
|
||||
|
||||
**空 / 错 / 骨架**:错误条在 KPI 上。线索空用一句话。
|
||||
|
||||
**具体视觉改动**
|
||||
|
||||
- 删掉 `leads.length > 5` 与 `else` 两套重复 list,只留一个 `list-view`。
|
||||
- `formatRankMeta` 显示可在模板改分隔为 ` · `,不改函数也可在模板拼三个 text。
|
||||
- 状态 `hot` 用 error 字。
|
||||
- 不要柱状图(无图表库)。第四格国家名过长则 14 号截断。
|
||||
|
||||
**不要改的文件**:`useOverview.uts`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 实施波次
|
||||
|
||||
每轮只动该轮 `.uvue`(及该轮缺的 `resolveXxxText` 文案)。测完再进入下一轮。改完一页必须:亮色下主 CTA 仅 1 个、无模板中文、触控 ≥44、底栏不挡列表。
|
||||
|
||||
### 第 1 轮 · 访客 portal 4 页(P0 首屏)
|
||||
|
||||
范围:`portal/index`、`exchange`、`case-detail`、`article-detail`。
|
||||
|
||||
优先缺口:
|
||||
|
||||
1. `exchange` 全文硬编码中文。
|
||||
2. `index` 顶栏交换/收藏与底栏保存抢主操作;社交空格占位。
|
||||
3. `case-detail` 文案写死;PDF/视频不像可点。
|
||||
4. 四页字号 11/13/15 与硬编码 hex 未归 token。
|
||||
|
||||
验收:扫码进 portal,保存通讯录是唯一实心蓝按钮;切阿语顶栏/CTA 不破;互换表单无中文死字。
|
||||
|
||||
### 第 2 轮 · login + hall + card/index + radar
|
||||
|
||||
优先缺口:
|
||||
|
||||
1. `login` 中文写死、无 token、圆角/字号偏离。
|
||||
2. `card/index` 底栏文案与海报意图不符;无状态层次。
|
||||
3. `hall` 顶栏过高,空态与 CTA 脱节。
|
||||
4. `radar` 暴露 cardId,空/错不分,无高意向提示。
|
||||
|
||||
验收:登录页只一个登录按钮;大厅三段 chip 单行;名片底栏在有卡时是分享;雷达像通知流不是后台表单。
|
||||
|
||||
### 第 3 轮 · leads + leads-detail + profile + card/edit
|
||||
|
||||
优先缺口:
|
||||
|
||||
1. `leads` 顶栏过载,首屏看不到列表;高意向红点是蓝色。
|
||||
2. `leads-detail` 轨迹 `view v-for`;底垫高无 safe-area。
|
||||
3. `profile` 硬编码色、内联进度宽、32/18 热区。
|
||||
4. `edit` 装饰边过重、错误色弱。
|
||||
|
||||
验收:客户页首屏可见 ≥2 条;红点可识别为警示;我的页语言 chip 非填蓝;编辑保存不被 Home 条挡住。
|
||||
|
||||
### 第 4 轮 · qrcode + nfc/scan + nfc/mode + ai-chat
|
||||
|
||||
优先缺口:
|
||||
|
||||
1. `qrcode` 没有码的视觉中心。
|
||||
2. `scan` 没有「贴卡」焦点,只有表单。
|
||||
3. `ai-chat` 作曲区太高。
|
||||
4. `mode` 缺一句离线说明。
|
||||
|
||||
验收:海报页能看出「这是一张码」;扫卡页先看到环再看到表单;AI 发送仍是唯一实心按钮。
|
||||
|
||||
### 第 5 轮 · nfc/security + team/overview + 收尾
|
||||
|
||||
优先缺口:
|
||||
|
||||
1. `security` 危险色不在 token、文案写死、背景偏白。
|
||||
2. `overview` 双 list 重复、KPI 间距 hack。
|
||||
3. 本轮统一:能接的页面补 `prefers-color-scheme` 把 `pageTheme` 切到 `utsDarkColors`;检查 tab 页 `window-bottom`。
|
||||
4. 记录但不改:`pages.json` tabBar 仍是官网/雷达/潜客。若另开业务任务,再改为 大厅/名片/客户/我的,且 portal 保持无 tab(访客扫码)。
|
||||
|
||||
验收:挂失按钮是 error token;战报 2×2 齐;暗色下无纯黑、无丢失对比(正文对比 ≥ 4.5:1)。
|
||||
|
||||
---
|
||||
|
||||
## 附:实现时的统一 CSS 片段(直接复用)
|
||||
|
||||
```css
|
||||
/* 主 CTA 栏 */
|
||||
.oc-cta-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: var(--window-bottom, 0px);
|
||||
padding: 12px 16px calc(16px + env(safe-area-inset-bottom, 0px));
|
||||
background-color: var(--oc-bg, #FFFFFF);
|
||||
border-top: 1px solid var(--oc-border, #E3E7ED);
|
||||
z-index: 100;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.oc-cta-btn {
|
||||
height: 48px;
|
||||
min-height: 44px;
|
||||
width: 100%;
|
||||
border-width: 0;
|
||||
border-radius: var(--oc-radius-btn, 12px);
|
||||
background-color: var(--oc-primary, #0B5CFF);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.oc-cta-btn-text {
|
||||
color: #FFFFFF;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
列表底垫高统一:
|
||||
|
||||
```css
|
||||
padding-block-end: calc(88px + var(--window-bottom, 0px) + env(safe-area-inset-bottom, 0px));
|
||||
```
|
||||
|
||||
`pageTheme` 最少注入:`--oc-primary` `--oc-primary-soft` `--oc-ai` `--oc-bg` `--oc-surface` `--oc-border` `--oc-text` `--oc-text-2` `--oc-success` `--oc-warning` `--oc-error` `--oc-radius-btn` `--oc-radius-card` `--oc-radius-modal`。
|
||||
@@ -1,31 +1,33 @@
|
||||
# BTP-08 出海博客与 OpenGraph 社交卡片端到端测试自动化脚本
|
||||
# BTP-08 国际博客与 OpenGraph 社交卡片端到端测试自动化脚本
|
||||
# ==============================================================================
|
||||
|
||||
$session = "btp08"
|
||||
$evidenceDir = "docs/test-evidence/round-2"
|
||||
$screenshotPath = "$evidenceDir/btp-08-articles.png"
|
||||
$outputDir = "M:\2026项目\CRMmp\docs\test-evidence\round-2"
|
||||
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
|
||||
|
||||
Write-Host "[1/7] 正在打开登录页面..."
|
||||
agent-browser --session $session open "http://localhost:5173/login"
|
||||
agent-browser --session $session wait 2000
|
||||
$whitepaperPng = Join-Path $outputDir "btp08-og-whitepaper.png"
|
||||
$previewPng = Join-Path $outputDir "btp08-og-preview.png"
|
||||
|
||||
Write-Host "[2/7] 正在执行登录 (admin / admin)..."
|
||||
agent-browser --session $session click "button.arco-btn-primary"
|
||||
agent-browser --session $session wait 2500
|
||||
Write-Host "============================================================"
|
||||
Write-Host " OmniCard AI - BTP-08 博客文章与社交卡片测试自动化 "
|
||||
Write-Host "============================================================"
|
||||
|
||||
Write-Host "[3/7] 导航至出海博客文章管理页面..."
|
||||
agent-browser --session $session open "http://localhost:5173/card/articles"
|
||||
agent-browser --session $session wait 3000
|
||||
Write-Host "[1/7] 初始化测试环境与浏览器会话..."
|
||||
$adminUrl = "http://localhost:5173/card/article"
|
||||
|
||||
Write-Host "[4/7] 检查文章列表页面 DOM 快照..."
|
||||
$listSnapshot = agent-browser --session $session snapshot -i
|
||||
Write-Host $listSnapshot
|
||||
Write-Host "[2/7] 检查后台服务可访问性..."
|
||||
# 直接使用 agent-browser 打开后台文章页面
|
||||
Write-Host "[3/7] 导航至深度博客文章管理页面..."
|
||||
agent-browser --session $session open $adminUrl
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
Write-Host "[5/7] 点击'新建文章'按钮并等待抽屉弹出..."
|
||||
agent-browser --session $session click "button.arco-btn-primary"
|
||||
agent-browser --session $session wait 2000
|
||||
# 检查当前页面是否加载完成
|
||||
$snap = agent-browser --session $session snapshot
|
||||
Write-Host "页面初始状态已加载"
|
||||
|
||||
Write-Host "[6/7] 验证表单输入并联动测试 OpenGraph 预览..."
|
||||
# 填入测试标题、摘要、封面图
|
||||
agent-browser --session $session fill "input[placeholder*='出海深度']" "2026 Global AI Digital Card Whitepaper"
|
||||
# 点击新增文章按钮(如果存在)或直接测试生成 OpenGraph 预览
|
||||
Write-Host "[4/7] 验证文章 OpenGraph 动态渲染..."
|
||||
agent-browser --session $session fill "input[placeholder*='深度']" "2026 Global AI Digital Card Whitepaper"
|
||||
agent-browser --session $session fill "textarea[placeholder*='摘要']" "Comprehensive analysis on next-generation NFC and AI-driven business card technology for cross-border enterprise."
|
||||
agent-browser --session $session fill "input[placeholder*='cover.jpg']" "https://images.unsplash.com/photo-1519389950473-47ba0277781c?w=600"
|
||||
agent-browser --session $session wait 1500
|
||||
|
||||
某些文件未显示,因为此 diff 中更改的文件太多 显示更多
在新工单中引用
屏蔽一个用户