Phase 2 实施计划:老板体验升级¶
创建日期:2026-02-14 前置文档:首页 UI 改进计划 · Phase Roadmap 目标:将 UI 改进计划落地为具体的编码任务清单,覆盖 studio-api 和 studio-web 两端
1. 实施总览¶
1.1 改动范围¶
graph TB
subgraph backend["studio-api 后端"]
A1["新增 /v2/dashboard/summary API"]
A2["新增 /v2/leads/follow-ups API"]
A3["现有 /v2/leads 增加 temperature 字段"]
A4["新增 /v2/staff/quick-stats API"]
end
subgraph frontend["studio-web 前端"]
F1["TodaySummaryBar 组件"]
F2["ActionQueue 组件"]
F3["HotLeadsPanel 组件"]
F4["StaffQuickView 组件"]
F5["Dashboard 页面重构"]
F6["CollapsibleAnalytics 折叠面板"]
end
A1 --> F1
A2 --> F2
A3 --> F3
A4 --> F4
F1 --> F5
F2 --> F5
F3 --> F5
F4 --> F5
F6 --> F5
1.2 实施批次与顺序¶
Batch 0 (准备) ─── API 类型定义 + TanStack Query composables
│
Batch 1 (P0 核心) ─── Summary API ──→ TodaySummaryBar 组件
─── Follow-up API ──→ ActionQueue 组件
│
Batch 2 (P1 增强) ─── Lead temperature ──→ HotLeadsPanel 组件
─── Staff quick-stats ──→ StaffQuickView 组件
│
Batch 3 (P2 打磨) ─── Dashboard 页面组装 + 折叠面板 + 响应式 + 空状态
2. Batch 0:准备工作¶
2.1 共享类型定义¶
文件:studio-api/packages/types/src/dashboard.ts (新建)
// ===== Today Summary =====
export interface TodaySummaryMetrics {
totalCalls: number
totalCallsDelta: number // 百分比变化, e.g. 20 = +20%
bookings: number
bookingsDelta: number
pendingFollowUps: number
pendingFollowUpsDelta: number
avgResponseTimeMinutes: number
avgResponseTimeDelta: number // 负数=变快=变好
}
export interface TodaySummaryInsights {
topPerformer: {
name: string
bookingRate: number // 0-100
} | null
urgentLeads: number // > 24h 未跟进的 Lead 数
}
export interface TodaySummaryResponse {
date: string // ISO date "2026-02-14"
metrics: TodaySummaryMetrics
insights: TodaySummaryInsights
}
// ===== Follow-up Queue =====
export type SlaStatus = 'critical' | 'warning' | 'ok'
export interface FollowUpItem {
leadId: string
leadName: string
phone: string
reason: string
callStartTime: string // ISO datetime
hoursAgo: number
slaStatus: SlaStatus
staffName: string
callSessionId: string // 用于跳转到通话详情
}
export interface FollowUpQueueResponse {
total: number
items: FollowUpItem[]
}
// ===== Lead Temperature =====
export type LeadTemperature = 'hot' | 'warm' | 'cold'
// 在现有 Lead 类型上增加:
// temperature: LeadTemperature
// callCount: number
// lastContactTime: string | null
// daysSinceReceived: number
// ===== Staff Quick Stats =====
export interface StaffQuickStat {
name: string
totalCalls: number
bookings: number
bookingRate: number // 0-100
bookingRateDelta: number // vs 上一周期
trend: 'up' | 'down' | 'flat'
}
export interface StaffQuickStatsResponse {
period: string // "today" | "this_week"
staff: StaffQuickStat[] // 按 bookingRate 降序
}
2.2 前端 Query Composables¶
文件:studio-web/src/composables/use-home-dashboard.ts (新建)
import { useQuery } from '@tanstack/vue-query'
export function useTodaySummary(orgId: Ref<string>, siteId: Ref<string>) {
return useQuery({
queryKey: ['dashboard', 'summary', orgId, siteId, today()],
queryFn: () => otApi.dashboard.getSummary(orgId.value, siteId.value),
refetchInterval: 5 * 60 * 1000,
staleTime: 2 * 60 * 1000,
})
}
export function useFollowUpQueue(orgId: Ref<string>, siteId: Ref<string>) {
return useQuery({
queryKey: ['leads', 'follow-ups', orgId, siteId],
queryFn: () => otApi.leads.getFollowUps(orgId.value, siteId.value),
refetchInterval: 2 * 60 * 1000,
staleTime: 60 * 1000,
})
}
export function useStaffQuickStats(orgId: Ref<string>, siteId: Ref<string>) {
return useQuery({
queryKey: ['staff', 'quick-stats', orgId, siteId, today()],
queryFn: () => otApi.staff.getQuickStats(orgId.value, siteId.value),
refetchInterval: 5 * 60 * 1000,
staleTime: 2 * 60 * 1000,
})
}
3. Batch 1:P0 核心功能¶
3.1 Task 1: Today Summary API¶
3.1.1 后端路由¶
文件:studio-api/apps/api/src/routes/v2/dashboard/summary.ts (新建)
GET /v2/dashboard/summary?orgId={orgId}&siteId={siteId}
Query Params:
- orgId: string (required)
- siteId: string (required)
- date: string (optional, default: today, format: YYYY-MM-DD)
Response: TodaySummaryResponse
3.1.2 后端 Service 层¶
文件:studio-api/apps/api/src/services/dashboard-summary.service.ts (新建)
逻辑流程:
1. 获取今天的通话数据
→ 查询 call-analysis 表: orgId + callStartTime 在今天
→ 计算: totalCalls, bookings (intro_booking + success), pendingFollowUps
2. 获取昨天同时段数据(用于 delta 计算)
→ 查询 call-analysis 表: orgId + callStartTime 在昨天 00:00 到昨天同一小时
→ 计算 delta: (today - yesterday) / yesterday * 100
3. 计算 Avg Response Time
→ 查询 Lead 表: receivedAt 在最近 7 天
→ 关联 call-analysis 表: 找每个 Lead 的首次通话
→ avgResponseTime = mean(firstCallTime - receivedAt)
→ delta vs 上周平均
4. 计算 Insights
→ Top Performer: 按 staff_name 分组,算 bookingRate,取最高
→ Urgent Leads: follow_up_needed=yes 且 hoursAgo > 24
返回 TodaySummaryResponse
DynamoDB 查询策略:
| 查询 | 表 | GSI | 条件 |
|---|---|---|---|
| 今日通话 | call-analysis | orgId-callStartTime-index |
orgId = X AND callStartTime BETWEEN today_start AND now |
| 昨日通话 | call-analysis | 同上 | orgId = X AND callStartTime BETWEEN yesterday_start AND yesterday_same_hour |
| 近期 Leads | LeadTracking-v2 | orgId-receivedAt-index |
orgId = X AND receivedAt > 7_days_ago |
| Follow-up 待处理 | call-analysis | orgId-callStartTime-index |
filter: follow_up_needed = yes |
3.1.3 前端组件: TodaySummaryBar¶
文件:studio-web/src/components/dashboard/TodaySummaryBar.vue (新建)
组件结构:
<template>
<Card class="...">
<CardHeader>
<div class="flex items-center justify-between">
<CardTitle>Today's Summary</CardTitle>
<span class="text-sm text-muted-foreground">{{ formattedDate }}</span>
</div>
</CardHeader>
<CardContent>
<!-- 4 个指标卡片 -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
<MiniMetricCard
v-for="metric in metrics"
:key="metric.key"
:label="metric.label"
:value="metric.value"
:delta="metric.delta"
:inverse="metric.inverse"
@click="metric.onClick"
/>
</div>
<!-- Insight 行 -->
<div class="mt-4 flex items-center gap-4 text-xs text-muted-foreground">
<span v-if="insights.topPerformer">
⭐ Top: {{ insights.topPerformer.name }}
({{ insights.topPerformer.bookingRate }}% booking rate)
</span>
<span v-if="insights.urgentLeads > 0" class="text-red-600 font-medium">
⚠ {{ insights.urgentLeads }} leads pending > 24h
</span>
</div>
</CardContent>
</Card>
</template>
子组件: MiniMetricCard
<!-- MiniMetricCard.vue -->
<template>
<button
class="rounded-lg border p-4 text-left hover:bg-accent/50 transition-colors"
@click="$emit('click')"
>
<div class="text-3xl font-bold tabular-nums">{{ displayValue }}</div>
<div class="text-sm text-muted-foreground mt-1">{{ label }}</div>
<div
v-if="delta !== undefined"
class="text-xs font-medium mt-1"
:class="deltaColor"
>
{{ deltaIcon }} {{ Math.abs(delta) }}%
</div>
</button>
</template>
3.2 Task 2: Follow-up Queue API + ActionQueue 组件¶
3.2.1 后端路由¶
文件:studio-api/apps/api/src/routes/v2/leads/follow-ups.ts (新建)
GET /v2/leads/follow-ups?orgId={orgId}&siteId={siteId}&limit=5
Query Params:
- orgId: string (required)
- siteId: string (required)
- limit: number (optional, default: 5, max: 50)
Response: FollowUpQueueResponse
3.2.2 后端 Service 层¶
文件:studio-api/apps/api/src/services/follow-up-queue.service.ts (新建)
逻辑流程:
1. 查询最近 7 天内 follow_up_needed = "yes" 的通话
→ call-analysis 表, orgId-callStartTime-index
→ filter: follow_up_needed = "yes"
2. 按 phone 分组,取最近一次通话
→ 避免同一客户出现多次
3. 排除已完成的 follow-up
→ 检查该 phone 在 follow-up 通话后是否有新通话
→ 如有新通话 → 视为已跟进,排除
4. 关联 Lead 信息
→ 用 phone 查 LeadTracking-v2 获取 leadName
5. 计算 SLA
→ hoursAgo = (now - callStartTime) / 3600000
→ slaStatus = hoursAgo >= 24 ? 'critical' : hoursAgo >= 4 ? 'warning' : 'ok'
6. 按 hoursAgo 降序排序(最紧急在前)
7. 截取 limit 条返回
3.2.3 前端组件: ActionQueue¶
文件:studio-web/src/components/dashboard/ActionQueue.vue (新建)
组件结构:
<template>
<Card>
<CardHeader>
<div class="flex items-center justify-between">
<CardTitle class="flex items-center gap-2">
Action Queue
<Badge variant="secondary">{{ total }} pending</Badge>
</CardTitle>
</div>
</CardHeader>
<CardContent class="p-0">
<!-- 空状态 -->
<EmptyState v-if="items.length === 0" icon="CheckCircle" message="All caught up!" />
<!-- Action Items -->
<div v-else class="divide-y">
<ActionItem
v-for="item in items"
:key="item.leadId"
:item="item"
@call="handleCall(item)"
@done="handleMarkDone(item)"
@click="handleOpenDetail(item)"
/>
</div>
<!-- Footer -->
<div v-if="total > items.length" class="p-4 border-t">
<RouterLink to="/lead-tracker?filter=follow-up" class="text-sm text-primary">
查看全部 {{ total }} 条 Follow-ups →
</RouterLink>
</div>
</CardContent>
</Card>
</template>
子组件: ActionItem
<!-- ActionItem.vue -->
<template>
<div
class="flex items-center gap-3 px-4 py-3 hover:bg-accent/50 cursor-pointer transition-colors"
@click="$emit('click')"
>
<!-- SLA 指示灯 -->
<div
class="size-2.5 rounded-full shrink-0"
:class="{
'bg-red-500': item.slaStatus === 'critical',
'bg-amber-500': item.slaStatus === 'warning',
'bg-green-500': item.slaStatus === 'ok',
}"
/>
<!-- 信息 -->
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="font-medium text-sm truncate">{{ item.leadName }}</span>
<span class="text-xs text-muted-foreground">·</span>
<span class="text-xs text-muted-foreground truncate">{{ item.reason }}</span>
</div>
<div class="text-xs text-muted-foreground mt-0.5">
{{ item.staffName }} · {{ formatTimeAgo(item.hoursAgo) }}
</div>
</div>
<!-- 操作按钮 -->
<div class="flex items-center gap-1 shrink-0" @click.stop>
<Button size="sm" variant="outline" @click="$emit('call')">
<Phone class="size-3.5" />
Call
</Button>
<Button size="sm" variant="ghost" @click="$emit('done')">
<Check class="size-3.5" />
</Button>
</div>
</div>
</template>
4. Batch 2:P1 增强功能¶
4.1 Task 3: Lead Temperature¶
4.1.1 后端改动¶
文件:studio-api/apps/api/src/services/lead.service.ts (修改)
在现有 Lead 列表查询结果上,增加 temperature 计算:
function calculateTemperature(lead: Lead, calls: Call[]): LeadTemperature {
const daysSinceReceived = differenceInDays(new Date(), new Date(lead.receivedAt))
const hasRecentCall = calls.some(c =>
differenceInHours(new Date(), new Date(c.callStartTime)) < 24
)
const hasFollowUpNeeded = calls.some(c => c.followUpNeeded === 'yes')
const hasSuccess = calls.some(c => c.outcome === 'success')
const attemptedCount = calls.filter(c => c.outcome === 'attempted').length
// Hot: 新 Lead 或 近期有互动 或 需要跟进
if (daysSinceReceived <= 3 || hasRecentCall || hasFollowUpNeeded) {
return 'hot'
}
// Warm: 中期 Lead,有通话但未成交
if (daysSinceReceived <= 7 && calls.length > 0 && !hasSuccess) {
return 'warm'
}
// Cold: 老 Lead 无互动 或 多次失败
return 'cold'
}
返回结构增强:在现有 GET /v2/leads 响应中增加字段:
// 在每个 Lead 对象上增加:
{
...existingLeadFields,
temperature: 'hot' | 'warm' | 'cold',
callCount: number,
lastContactTime: string | null,
daysSinceReceived: number,
}
4.1.2 前端组件: HotLeadsPanel¶
文件:studio-web/src/components/dashboard/HotLeadsPanel.vue (新建)
<template>
<Card class="h-full">
<CardHeader>
<div class="flex items-center justify-between">
<CardTitle>Active Leads</CardTitle>
<div class="flex items-center gap-2 text-xs">
<Badge variant="outline" class="text-red-500 border-red-200 bg-red-50">
🔥 {{ counts.hot }}
</Badge>
<Badge variant="outline" class="text-amber-500 border-amber-200 bg-amber-50">
🟡 {{ counts.warm }}
</Badge>
<Badge variant="outline" class="text-blue-500 border-blue-200 bg-blue-50">
❄️ {{ counts.cold }}
</Badge>
</div>
</div>
</CardHeader>
<CardContent class="p-0">
<div class="divide-y">
<LeadRow
v-for="lead in hotLeads.slice(0, 5)"
:key="lead.leadId"
:lead="lead"
@click="navigateToLead(lead)"
/>
</div>
<div class="p-4 border-t">
<RouterLink to="/lead-tracker" class="text-sm text-primary">
查看全部 Leads →
</RouterLink>
</div>
</CardContent>
</Card>
</template>
4.2 Task 4: Staff Quick Stats¶
4.2.1 后端路由¶
文件:studio-api/apps/api/src/routes/v2/staff/quick-stats.ts (新建)
GET /v2/staff/quick-stats?orgId={orgId}&siteId={siteId}&period=today
Query Params:
- orgId: string (required)
- siteId: string (required)
- period: "today" | "this_week" (optional, default: "today")
Response: StaffQuickStatsResponse
4.2.2 后端 Service 层¶
逻辑流程:
1. 查询指定时段内的所有通话
→ call-analysis 表, orgId-callStartTime-index
→ filter: siteId = X
2. 按 staff_name 分组
→ totalCalls = 该员工的通话数
→ bookings = subcategory=intro_booking + outcome=success 的数量
→ bookingRate = bookings / totalCalls * 100 (仅 revenue_impacting)
3. 计算 delta
→ 获取上一个同等时段的数据 (today → yesterday, this_week → last_week)
→ delta = currentRate - previousRate
→ trend = delta > 2 ? 'up' : delta < -2 ? 'down' : 'flat'
4. 按 bookingRate 降序排序
4.2.3 前端组件: StaffQuickView¶
文件:studio-web/src/components/dashboard/StaffQuickView.vue (新建)
<template>
<Card class="h-full">
<CardHeader>
<div class="flex items-center justify-between">
<CardTitle>Staff Performance</CardTitle>
<span class="text-xs text-muted-foreground">Today</span>
</div>
</CardHeader>
<CardContent class="p-0">
<div class="divide-y">
<div
v-for="staff in staffList"
:key="staff.name"
class="flex items-center gap-3 px-4 py-3 hover:bg-accent/50 cursor-pointer"
@click="navigateToStaff(staff.name)"
>
<Avatar class="size-8">
<AvatarFallback>{{ staff.name[0] }}</AvatarFallback>
</Avatar>
<div class="flex-1 min-w-0">
<div class="font-medium text-sm">{{ staff.name }}</div>
<div class="text-xs text-muted-foreground">
{{ staff.totalCalls }} calls · {{ staff.bookings }} bookings
</div>
</div>
<div class="text-right shrink-0">
<div class="font-semibold text-sm tabular-nums">
{{ staff.bookingRate }}%
</div>
<div class="text-xs" :class="trendColor(staff.trend)">
{{ trendIcon(staff.trend) }}
</div>
</div>
</div>
</div>
<div class="p-4 border-t">
<RouterLink to="/dashboard?tab=staff" class="text-sm text-primary">
查看详细绩效 →
</RouterLink>
</div>
</CardContent>
</Card>
</template>
5. Batch 3:P2 打磨¶
5.1 Task 5: Dashboard 页面重组¶
文件:studio-web/src/components/analytics/DashboardAnalytics.vue (修改)
改动:将现有的 DailyAnalyticsTab 内容重组为新布局
<template>
<div class="space-y-6">
<!-- 区域 A: Today's Summary -->
<TodaySummaryBar />
<!-- 区域 B: Action Queue -->
<ActionQueue />
<!-- 区域 C: 双栏 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<HotLeadsPanel />
<StaffQuickView />
</div>
<!-- 区域 D: 折叠面板 — 现有深度分析 -->
<CollapsibleAnalytics>
<!-- 移入现有内容 -->
<PerformanceOverview />
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<DonutChartCategories />
<FollowUpReasons />
<RevenueWaterfallSankey />
</div>
</CollapsibleAnalytics>
<!-- Tabs: Trends + Staff Performance (保持不变) -->
<Tabs default-value="trends">
<TabsList>
<TabsTrigger value="trends">Trends</TabsTrigger>
<TabsTrigger value="staff">Staff Performance</TabsTrigger>
</TabsList>
<TabsContent value="trends">
<TrendsTab />
</TabsContent>
<TabsContent value="staff">
<StaffPerformance />
</TabsContent>
</Tabs>
</div>
</template>
5.2 Task 6: CollapsibleAnalytics 折叠面板¶
文件:studio-web/src/components/dashboard/CollapsibleAnalytics.vue (新建)
<template>
<Collapsible v-model:open="isOpen">
<Card>
<CollapsibleTrigger as-child>
<CardHeader class="cursor-pointer hover:bg-accent/50 transition-colors">
<div class="flex items-center justify-between">
<CardTitle>Detailed Analytics</CardTitle>
<ChevronRight
class="size-4 transition-transform"
:class="{ 'rotate-90': isOpen }"
/>
</div>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent>
<slot />
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
</template>
<script setup lang="ts">
const isOpen = useLocalStorage('dashboard-analytics-expanded', false)
</script>
5.3 Task 7: 响应式适配与空状态¶
响应式断点:
| 区域 | Mobile (< 768) | Tablet (768-1023) | Desktop (≥ 1024) |
|---|---|---|---|
| Summary | 2x2 grid, text-2xl | 2x2 grid | 4 cols, text-3xl |
| Action Queue | 全宽, py-4 (touch) | 全宽 | 全宽 |
| Hot Leads / Staff | 堆叠, 各 3 条 | 堆叠 | 并排 2 列 |
| Collapsible | 默认收起, 不自动展开 | 默认收起 | 默认收起 |
空状态组件复用:使用现有的 empty state 模式,统一风格
6. API 实现细节¶
6.1 DynamoDB 查询优化¶
问题:Summary API 需要跨 call-analysis 和 LeadTracking-v2 两张表查询
方案:并行查询 + 内存聚合
async function getDashboardSummary(orgId: string, siteId: string, date: string) {
// 并行发起 4 个查询
const [todayCalls, yesterdayCalls, recentLeads, followUps] = await Promise.all([
queryCallsByDateRange(orgId, siteId, todayStart, now),
queryCallsByDateRange(orgId, siteId, yesterdayStart, yesterdaySameHour),
queryRecentLeads(orgId, siteId, sevenDaysAgo),
queryFollowUpCalls(orgId, siteId, sevenDaysAgo),
])
// 内存聚合
return {
metrics: computeMetrics(todayCalls, yesterdayCalls, recentLeads),
insights: computeInsights(todayCalls, followUps),
}
}
6.2 缓存策略¶
| API | 服务端缓存 | 客户端 staleTime | 客户端 refetch |
|---|---|---|---|
/v2/dashboard/summary |
无(实时) | 2 min | 每 5 min |
/v2/leads/follow-ups |
无(实时) | 1 min | 每 2 min |
/v2/leads (温度) |
无 | 2 min | 手动刷新 |
/v2/staff/quick-stats |
无 | 2 min | 每 5 min |
6.3 错误处理¶
// 前端: 各区域独立加载,互不阻塞
// Summary 失败不影响 Action Queue 显示
// 每个区域独立的 error boundary:
<template>
<TodaySummaryBar /> <!-- 失败显示 "Unable to load summary" -->
<ActionQueue /> <!-- 独立加载 -->
<div class="grid ...">
<HotLeadsPanel /> <!-- 独立加载 -->
<StaffQuickView /> <!-- 独立加载 -->
</div>
</template>
7. 文件变更清单¶
7.1 studio-api (后端)¶
| 操作 | 文件路径 | 说明 |
|---|---|---|
| 新建 | packages/types/src/dashboard.ts |
类型定义 |
| 新建 | apps/api/src/routes/v2/dashboard/summary.ts |
Summary 路由 |
| 新建 | apps/api/src/services/dashboard-summary.service.ts |
Summary 业务逻辑 |
| 新建 | apps/api/src/routes/v2/leads/follow-ups.ts |
Follow-up 路由 |
| 新建 | apps/api/src/services/follow-up-queue.service.ts |
Follow-up 业务逻辑 |
| 新建 | apps/api/src/routes/v2/staff/quick-stats.ts |
Staff 速览路由 |
| 新建 | apps/api/src/services/staff-quick-stats.service.ts |
Staff 速览逻辑 |
| 修改 | apps/api/src/services/lead.service.ts |
增加 temperature 计算 |
| 修改 | apps/api/src/routes/v2/index.ts |
注册新路由 |
7.2 studio-web (前端)¶
| 操作 | 文件路径 | 说明 |
|---|---|---|
| 新建 | src/composables/use-home-dashboard.ts |
数据获取 composables |
| 新建 | src/api/ot/dashboard.ts |
Dashboard API 调用 |
| 新建 | src/components/dashboard/TodaySummaryBar.vue |
今日概要条 |
| 新建 | src/components/dashboard/MiniMetricCard.vue |
迷你指标卡 |
| 新建 | src/components/dashboard/ActionQueue.vue |
待办队列 |
| 新建 | src/components/dashboard/ActionItem.vue |
待办行项目 |
| 新建 | src/components/dashboard/HotLeadsPanel.vue |
Hot Lead 面板 |
| 新建 | src/components/dashboard/StaffQuickView.vue |
员工速览 |
| 新建 | src/components/dashboard/CollapsibleAnalytics.vue |
折叠分析面板 |
| 修改 | src/components/analytics/DashboardAnalytics.vue |
页面布局重组 |
8. 验收标准¶
8.1 功能验收¶
| # | 验收项 | 通过条件 |
|---|---|---|
| 1 | 登录后首屏展示 Today's Summary | 4 个 KPI 在首屏可见,有对比箭头 |
| 2 | Action Queue 展示待跟进 | 按 SLA 紧急度排序,颜色正确 |
| 3 | Action Queue [Call] 按钮 | 复制号码到剪贴板或触发拨号 |
| 4 | Action Queue [Done] 按钮 | 乐观更新移除,后端同步 |
| 5 | Hot Leads 温度正确 | Hot/Warm/Cold 规则与文档一致 |
| 6 | Staff Quick View 排序 | 按 bookingRate 降序 |
| 7 | 折叠面板保持状态 | 刷新后记住展开/收起状态 |
| 8 | 各区域独立加载 | 一个 API 失败不阻塞其他区域 |
| 9 | 空状态 | 无数据时显示友好提示 |
| 10 | 响应式 | 手机/平板/桌面三端可用 |
8.2 性能验收¶
| 指标 | 目标 |
|---|---|
| 首屏 TTI (Time to Interactive) | < 2s |
| Summary API 响应 | < 500ms |
| Follow-up API 响应 | < 300ms |
| 首页总 API 调用数 | ≤ 4 (Summary + FollowUps + Leads + Staff) |
| Bundle size 增量 | < 15KB (gzipped) |
9. 风险与注意事项¶
| 风险 | 影响 | 缓解措施 |
|---|---|---|
| Summary API 跨表查询性能 | 高并发时延迟 | 并行查询 + 考虑后续加服务端缓存 |
| Follow-up "已完成" 判断不准 | 显示已跟进的 Lead | 严格匹配规则:同一 phone 有新通话 = 已跟进 |
| Lead 温度规则需要调优 | Hot 太多或太少 | 先上线,根据实际数据调整阈值 |
| 手机端布局过于拥挤 | 信息密度太高 | 移动端减少显示条数,折叠更多内容 |
| 现有 Tab 结构改动 | 老用户不习惯 | Analytics Tab 内容全部保留在折叠面板内,不删除 |