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 自动化单元测试套件并通过全部验证
这个提交包含在:
@@ -1325,6 +1325,235 @@ onUnload(() => {
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
|
||||
/* 6. 商务预约排期卡片与抽屉样式 (Bento Grid Appointment & Bottom Sheet) */
|
||||
.appointment-card {
|
||||
background: linear-gradient(135deg, #FFFFFF 0%, #F5F9FF 100%);
|
||||
border: 1px solid rgba(0, 136, 255, 0.15);
|
||||
}
|
||||
.appointment-tag {
|
||||
background-color: var(--oc-primary-soft, #EBF5FF);
|
||||
}
|
||||
.appointment-card-body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.appointment-info-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.appointment-card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--oc-text, #101828);
|
||||
margin-block-end: 4px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.appointment-card-desc {
|
||||
font-size: 13px;
|
||||
color: var(--oc-text-2, #64748B);
|
||||
line-height: 1.45;
|
||||
margin-block-end: 8px;
|
||||
}
|
||||
.appointment-tz-badge {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 3px 8px;
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid var(--oc-border, #EBF1F7);
|
||||
border-radius: 9999px;
|
||||
width: fit-content;
|
||||
}
|
||||
.appointment-tz-text {
|
||||
font-size: 11px;
|
||||
color: var(--oc-text-2, #64748B);
|
||||
font-weight: 500;
|
||||
}
|
||||
.appointment-action-pill {
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
min-width: 90px;
|
||||
padding: 0 16px;
|
||||
background: var(--oc-primary-gradient, linear-gradient(135deg, #00A3FF 0%, #0B63E5 100%));
|
||||
border-radius: var(--oc-radius-pill, 9999px);
|
||||
border-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 136, 255, 0.2);
|
||||
cursor: pointer;
|
||||
}
|
||||
.appointment-action-pill:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.appointment-action-text {
|
||||
color: #FFFFFF;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 预约底部抽屉 */
|
||||
.appointment-sheet-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(16, 24, 40, 0.45);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.appointment-sheet {
|
||||
background-color: #FFFFFF;
|
||||
border-top-left-radius: 24px;
|
||||
border-top-right-radius: 24px;
|
||||
padding: 12px 20px calc(24px + env(safe-area-inset-bottom, 0px));
|
||||
box-shadow: var(--oc-shadow-float, 0 12px 36px rgba(16, 24, 40, 0.08));
|
||||
}
|
||||
.sheet-handle {
|
||||
width: 36px;
|
||||
height: 4px;
|
||||
background-color: #D0D5DD;
|
||||
border-radius: 2px;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
.sheet-header {
|
||||
margin-block-end: 16px;
|
||||
}
|
||||
.sheet-title-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-block-end: 4px;
|
||||
}
|
||||
.sheet-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--oc-text, #101828);
|
||||
}
|
||||
.sheet-close {
|
||||
font-size: 18px;
|
||||
color: #98A2B3;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.sheet-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--oc-text-2, #64748B);
|
||||
}
|
||||
.sheet-meeting-details {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background-color: var(--oc-bg, #F5F8FC);
|
||||
border-radius: 12px;
|
||||
padding: 10px 14px;
|
||||
margin-block-end: 16px;
|
||||
}
|
||||
.sheet-detail-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.sheet-detail-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #98A2B3;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.sheet-detail-val {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--oc-text, #101828);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.calendar-channels-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.calendar-channel-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 14px;
|
||||
min-height: 44px;
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid var(--oc-border, #EBF1F7);
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
transition: border-color 150ms ease;
|
||||
}
|
||||
.calendar-channel-item:active {
|
||||
background-color: #F8FAFC;
|
||||
border-color: #B2D7FF;
|
||||
}
|
||||
.channel-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.channel-icon-badge {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.google-badge {
|
||||
background-color: #FEF3F2;
|
||||
}
|
||||
.outlook-badge {
|
||||
background-color: #EFF8FF;
|
||||
}
|
||||
.ics-badge {
|
||||
background-color: #ECFDF3;
|
||||
}
|
||||
.channel-icon-text {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.google-badge .channel-icon-text {
|
||||
color: #D92D20;
|
||||
}
|
||||
.outlook-badge .channel-icon-text {
|
||||
color: #026AA2;
|
||||
}
|
||||
.ics-badge .channel-icon-text {
|
||||
color: #027A48;
|
||||
}
|
||||
.channel-texts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.channel-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--oc-text, #101828);
|
||||
}
|
||||
.channel-sub {
|
||||
font-size: 11px;
|
||||
color: var(--oc-text-2, #64748B);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.channel-action-arrow {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #98A2B3;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation: none !important;
|
||||
|
||||
@@ -435,6 +435,31 @@ export function resolvePortalCollectText(
|
||||
return getOmniText(loc, 'portal.collect');
|
||||
}
|
||||
|
||||
/**
|
||||
* 预约动作雷达轨迹上报,自动推进 CRM 商机流转
|
||||
*/
|
||||
export async function trackAppointmentAction(
|
||||
state: PortalState,
|
||||
meetingSummary?: string
|
||||
): Promise<boolean> {
|
||||
if (state.card == null || state.card!.id == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await recordRadarTrack({
|
||||
cardId: state.card!.id,
|
||||
visitorId: state.visitorId,
|
||||
touchType: 'appointment',
|
||||
stayDurationMs: Math.max(0, Date.now() - state.enterTime),
|
||||
scrollDepth: 100,
|
||||
});
|
||||
return true;
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const OWNER_CARD_IDS_STORAGE_KEY = 'omni_owner_card_ids';
|
||||
|
||||
/**
|
||||
|
||||
在新工单中引用
屏蔽一个用户