跳转至

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 内容全部保留在折叠面板内,不删除