feat(mpuniapp): 落地微官网商务日程预约与日历排期功能 (TDD)

- 新增 common/calendar.uts 日历工具,支持 Google Calendar、Outlook Live 与后端 RFC 5545 ICS 一键下载
- 扩展 common/types.uts,支持 appointment 卡片类型与日历预约参数契约
- 补齐 common/locale.uts 全球 7 种语言(含阿语 RTL)商务预约词条
- 在 pages/portal/index.uvue 中集成 Bento 预约卡片与底部预约抽屉 (Bottom Sheet)
- 在 pages/portal/usePortal.uts 中增加预约雷达轨迹上报 (touchType: appointment) 闭环联动 CRM
- 新增 mpuniapp/tests/appointment.test.ts 自动化单元测试套件并通过全部验证
这个提交包含在:
wikiboss
2026-09-21 15:16:39 +08:00
父节点 e4882c7707
当前提交 2d3ff2f5bd
共有 6 个文件被更改,包括 600 次插入 和 4 次删除

120
mpuniapp/common/calendar.uts 普通文件
查看文件

@@ -0,0 +1,120 @@
/**
/**
* OmniCard AI (CRMmp) - 商务日程预约与日历服务 (UTS 强类型版本)
* 严格对齐 backend/src/modules/card/service/calendar.ts 规范
*/
import { MeetingInviteOptions, CalendarInviteBundle } from './types.uts';
import { getBaseUrl } from './request.uts';
/**
* 格式化 UTC 时间为 RFC 5545 日期时间戳 (YYYYMMDDTHHmmssZ)
*/
export function formatIcsDate(date: Date | string | number): string {
const d = date instanceof Date ? date : new Date(date);
if (isNaN(d.getTime())) {
return '';
}
const pad = (n: number): string => (n < 10 ? '0' + n : String(n));
const year = d.getUTCFullYear();
const month = pad(d.getUTCMonth() + 1);
const day = pad(d.getUTCDate());
const hours = pad(d.getUTCHours());
const minutes = pad(d.getUTCMinutes());
const seconds = pad(d.getUTCSeconds());
return `${year}${month}${day}T${hours}${minutes}${seconds}Z`;
}
/**
* 格式化为 ISO 8601 字符串 (用于 Outlook)
*/
export function toIsoDate(date: Date | string | number): string {
const d = date instanceof Date ? date : new Date(date);
if (isNaN(d.getTime())) {
return '';
}
return d.toISOString();
}
/**
* 生成 Google Calendar Web 一键预约 URL
*/
export function generateGoogleCalendarUrl(meeting: MeetingInviteOptions): string {
const summary = meeting.summary || meeting.title || '商务洽谈';
const dtStart = formatIcsDate(meeting.startTime);
const dtEnd = formatIcsDate(meeting.endTime);
let details = meeting.description || '';
if (meeting.organizerEmail) {
details += `\nOrganizer: ${meeting.organizerEmail}`;
}
const queryParts: string[] = [
'action=TEMPLATE',
`text=${encodeURIComponent(summary)}`,
`dates=${encodeURIComponent(`${dtStart}/${dtEnd}`)}`,
`details=${encodeURIComponent(details.trim())}`,
`location=${encodeURIComponent(meeting.location || '')}`,
];
return `https://calendar.google.com/calendar/render?${queryParts.join('&')}`;
}
/**
* 生成 Outlook Live 一键预约 URL
*/
export function generateOutlookCalendarUrl(meeting: MeetingInviteOptions): string {
const summary = meeting.summary || meeting.title || '商务洽谈';
const startDt = toIsoDate(meeting.startTime);
const endDt = toIsoDate(meeting.endTime);
const queryParts: string[] = [
'path=%2Fcalendar%2Faction%2Fcompose',
'rru=addevent',
`subject=${encodeURIComponent(summary)}`,
`startdt=${encodeURIComponent(startDt)}`,
`enddt=${encodeURIComponent(endDt)}`,
`body=${encodeURIComponent(meeting.description || '')}`,
`location=${encodeURIComponent(meeting.location || '')}`,
];
return `https://outlook.live.com/calendar/0/deeplink/compose?${queryParts.join('&')}`;
}
/**
* 获取后端 RFC 5545 ICS 订阅文件下载 URL
*/
export function getMeetingIcsDownloadUrl(meeting: MeetingInviteOptions): string {
const baseUrl = getBaseUrl();
const summary = meeting.summary || meeting.title || '商务洽谈';
const dtStart = formatIcsDate(meeting.startTime);
const dtEnd = formatIcsDate(meeting.endTime);
const queryParts: string[] = [
`summary=${encodeURIComponent(summary)}`,
`startTime=${encodeURIComponent(dtStart)}`,
`endTime=${encodeURIComponent(dtEnd)}`,
`description=${encodeURIComponent(meeting.description || '')}`,
`location=${encodeURIComponent(meeting.location || '')}`,
];
if (meeting.organizerName) {
queryParts.push(`organizerName=${encodeURIComponent(meeting.organizerName)}`);
}
if (meeting.organizerEmail) {
queryParts.push(`organizerEmail=${encodeURIComponent(meeting.organizerEmail)}`);
}
return `${baseUrl}/open/card/portal/meeting-ics?${queryParts.join('&')}`;
}
/**
* 综合生成全平台日历预约包
*/
export function buildCalendarInviteBundle(meeting: MeetingInviteOptions): CalendarInviteBundle {
return {
googleUrl: generateGoogleCalendarUrl(meeting),
outlookUrl: generateOutlookCalendarUrl(meeting),
icsUrl: getMeetingIcsDownloadUrl(meeting),
};
}

查看文件

@@ -68,7 +68,16 @@ export type OmniMessageKey =
| 'portal.copied_email'
| 'portal.copied_telegram'
| 'portal.copied_twitter'
| 'portal.copied_wechat';
| 'portal.copied_wechat'
| 'portal.collect'
| 'portal.collected'
| 'portal.appointment'
| 'portal.appointment_desc'
| 'portal.book_now'
| 'portal.google_cal'
| 'portal.outlook_cal'
| 'portal.ics_download'
| 'portal.copied_meeting_link';
export interface OmniLocaleConfig {
code: OmniLocale;
@@ -132,6 +141,15 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'portal.copied_telegram': '已复制 Telegram 链接',
'portal.copied_twitter': '已复制 X 链接',
'portal.copied_wechat': '已复制微信号',
'portal.collect': '收入大厅',
'portal.collected': '已收入大厅',
'portal.appointment': '商务预约',
'portal.appointment_desc': '预约 30 分钟跨国视频会谈或展会现场洽谈',
'portal.book_now': '立即排期',
'portal.google_cal': 'Google 日历',
'portal.outlook_cal': 'Outlook 日历',
'portal.ics_download': '下载日历文件 (ICS)',
'portal.copied_meeting_link': '已复制预约日历链接',
},
},
'en-US': {
@@ -188,6 +206,15 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'portal.copied_telegram': 'Telegram link copied',
'portal.copied_twitter': 'X link copied',
'portal.copied_wechat': 'WeChat ID copied',
'portal.collect': 'Save to Hall',
'portal.collected': 'Saved to Hall',
'portal.appointment': 'Schedule Meeting',
'portal.appointment_desc': 'Book a 30-min global video sync or in-person talk',
'portal.book_now': 'Book Now',
'portal.google_cal': 'Google Calendar',
'portal.outlook_cal': 'Outlook Calendar',
'portal.ics_download': 'Download ICS File',
'portal.copied_meeting_link': 'Calendar link copied',
},
},
'ar-SA': {
@@ -244,6 +271,15 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'portal.copied_telegram': 'تم نسخ رابط تيليجرام',
'portal.copied_twitter': 'تم نسخ رابط إكس',
'portal.copied_wechat': 'تم نسخ معرف وي تشات',
'portal.collect': 'حفظ في القاعة',
'portal.collected': 'تم الحفظ في القاعة',
'portal.appointment': 'حجز موعد',
'portal.appointment_desc': 'احجز اجتماع فيديو عالمي لمدة 30 دقيقة أو محادثة شخصية',
'portal.book_now': 'احجز الآن',
'portal.google_cal': 'تقويم Google',
'portal.outlook_cal': 'تقويم Outlook',
'portal.ics_download': 'تنزيل ملف التقويم (ICS)',
'portal.copied_meeting_link': 'تم نسخ رابط التقويم',
},
},
'es-ES': {
@@ -300,6 +336,15 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'portal.copied_telegram': 'Enlace de Telegram copiado',
'portal.copied_twitter': 'Enlace de X copiado',
'portal.copied_wechat': 'ID de WeChat copiado',
'portal.collect': 'Guardar en el hall',
'portal.collected': 'Guardado en el hall',
'portal.appointment': 'Agendar Reunión',
'portal.appointment_desc': 'Reserva una reunión virtual de 30 min o charla presencial',
'portal.book_now': 'Agendar Ahora',
'portal.google_cal': 'Google Calendar',
'portal.outlook_cal': 'Outlook Calendar',
'portal.ics_download': 'Descargar Archivo ICS',
'portal.copied_meeting_link': 'Enlace de calendario copiado',
},
},
'de-DE': {
@@ -356,6 +401,15 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'portal.copied_telegram': 'Telegram-Link kopiert',
'portal.copied_twitter': 'X-Link kopiert',
'portal.copied_wechat': 'WeChat-ID kopiert',
'portal.collect': 'In die Halle legen',
'portal.collected': 'In der Halle gespeichert',
'portal.appointment': 'Termin vereinbaren',
'portal.appointment_desc': 'Buchen Sie ein 30-minütiges globales Meeting',
'portal.book_now': 'Jetzt buchen',
'portal.google_cal': 'Google Kalender',
'portal.outlook_cal': 'Outlook Kalender',
'portal.ics_download': 'ICS-Datei herunterladen',
'portal.copied_meeting_link': 'Kalenderlink kopiert',
},
},
'fr-FR': {
@@ -412,6 +466,15 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'portal.copied_telegram': 'Lien Telegram copié',
'portal.copied_twitter': 'Lien X copié',
'portal.copied_wechat': 'Identifiant WeChat copié',
'portal.collect': 'Ajouter au hall',
'portal.collected': 'Ajouté au hall',
'portal.appointment': 'Prendre Rendez-vous',
'portal.appointment_desc': 'Réservez une réunion vidéo globale de 30 minutes',
'portal.book_now': 'Réserver maintenant',
'portal.google_cal': 'Google Agenda',
'portal.outlook_cal': 'Outlook Calendrier',
'portal.ics_download': 'Télécharger le fichier ICS',
'portal.copied_meeting_link': 'Lien de calendrier copié',
},
},
'ja-JP': {
@@ -468,6 +531,15 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'portal.copied_telegram': 'Telegram のリンクをコピーしました',
'portal.copied_twitter': 'X のリンクをコピーしました',
'portal.copied_wechat': 'WeChat ID をコピーしました',
'portal.collect': 'ホールに保存',
'portal.collected': 'ホールに保存しました',
'portal.appointment': '日程を予約する',
'portal.appointment_desc': '30分間のオンライン面談または商談を予約',
'portal.book_now': '今すぐ予約',
'portal.google_cal': 'Google カレンダー',
'portal.outlook_cal': 'Outlook カレンダー',
'portal.ics_download': 'ICSファイルをダウンロード',
'portal.copied_meeting_link': 'カレンダーのリンクをコピーしました',
},
},
};
@@ -495,7 +567,7 @@ function syncState(target: OmniLocale): void {
}
/**
* 将任意语言标识规范化为 7 种主流语言之一(回退 en-US 出海默认)
* 将任意语言标识规范化为 7 种主流语言之一(回退 en-US 国际默认)
*/
export function normalizeLocale(raw: string): OmniLocale {
if (!raw || typeof raw !== 'string') {
@@ -542,7 +614,7 @@ export function resolveDeviceLocale(raw: string): OmniLocale {
}
/**
* 根据出海访客的国家码映射主流语言或回退 fallback(默认 en-US)
* 根据访客的国家码映射主流语言或回退 fallback(默认 en-US)
*/
export function resolveGeoLocale(countryCode?: string, fallback: string = 'en-US'): OmniLocale {
if (!countryCode || !countryCode.trim()) {

查看文件

@@ -30,10 +30,11 @@ export interface CardProfile {
address?: string;
city?: string;
country?: string;
meetingUrl?: string;
[key: string]: any;
}
export type BentoBlockType = 'hero' | 'case' | 'article' | 'social' | 'video' | 'form';
export type BentoBlockType = 'hero' | 'case' | 'article' | 'social' | 'video' | 'form' | 'appointment';
export type BentoColSpan = 1 | 2;
@@ -58,6 +59,23 @@ export interface BrandKit {
primaryColor: string;
}
export interface MeetingInviteOptions {
summary?: string;
title?: string;
description?: string;
location?: string;
startTime: Date | string | number;
endTime: Date | string | number;
organizerName?: string;
organizerEmail?: string;
}
export interface CalendarInviteBundle {
googleUrl: string;
outlookUrl: string;
icsUrl: string;
}
export interface CardPortalDetail {
id: number | string;
slug?: string;
@@ -80,6 +98,7 @@ export interface CardPortalDetail {
status: CardStatus;
bentoBlocks: BentoBlockPublic[];
brandKit?: BrandKit;
meetingUrl?: string;
cachedAt?: number;
}