前端架构¶
1. 架构图¶
源文件:frontend-architecture.drawio(用 draw.io 打开编辑)
1.1 图例¶
| 属性 | 颜色 | 对应组件 |
|---|---|---|
| 外部服务 | 蓝色 | 用户浏览器、AWS Cognito、studio-api、Cloudflare Pages、Sentry |
| 第一层 + 第二层 | 橙色 | 应用入口(main.ts、路由、守卫)、页面层(认证页面、侧边栏所有页面) |
| 业务组件 | 紫色 | 数据可视化、通话分析、活动时间线、数据表、配置管理 |
| 原子组件 | 深紫色 | shadcn-vue UI 积木(表单、弹出层、数据展示、导航交互) |
| 逻辑层 | 绿色 | TanStack Query Hooks、业务 Composables、Pinia Stores |
| API 层 | 红色 | Axios 客户端、OT API 模块、RingCentral API、认证服务 |
| 数据流 | 彩色粗实线 | 用户请求 → 前端资源 → 路由导航 → API 调用 |
| 组件引用 | 灰色虚线 | 页面使用组件、调用逻辑层、状态读写 |
2. 数据流时序¶
用户登录后访问一个页面时,数据如何在五层之间流动:
sequenceDiagram
participant U as 用户浏览器
participant R as 路由守卫
participant P as 页面层<br>Dashboard
participant C as 组件层<br>MetricCard / Chart
participant L as 逻辑层<br>useCalls()
participant A as API 层<br>Axios
participant S as studio-api
U->>R: 访问 /dashboard
R->>R: 检查 authStore.isLogin
alt 未登录
R-->>U: 重定向 /auth/sign-in
end
R->>P: 放行,渲染 Dashboard 页面
P->>C: 挂载业务组件(图表、表格...)
C->>L: 调用 useCalls(storeId, dateRange)
L->>L: TanStack Query 检查缓存
alt 缓存命中
L-->>C: 返回缓存数据
end
L->>A: 缓存未命中,发起请求
A->>A: 拦截器附加 Bearer token
A->>S: GET /calls?storeId=xxx
S-->>A: JSON 响应
A-->>L: 响应数据
L->>L: 写入缓存
L-->>C: 返回数据(Vue 反应式)
C-->>P: 组件重新渲染
P-->>U: 页面显示数据
3. 分层架构¶
前端采用五层架构,数据自上而下流动,每层职责清晰:
3.1 第一层:应用入口¶
main.ts 启动应用
→ Amplify.configure({...})(pre-bootstrap,不是 plugin)
→ setupPlugins(app):Dayjs / NProgress / ECharts / AutoAnimate / TanStack Query / i18n / Pinia(共 7 个 plugin)
→ 创建路由(Vue Router + unplugin-vue-router 文件路由)
→ 路由守卫链:dateRedirect → datePreservation → commonGuard(nprogress)→ authGuard → analytics(5 步)
→ 挂载 App.vue(Suspense + RouterView + Toaster)
3.2 第二层:页面层¶
使用 unplugin-vue-router 文件路由,src/pages/ 目录结构自动生成路由。下表基于 2026-04-23 find src/pages -name "*.vue" 实查(共 117 文件 / 30+ 顶层目录)。
认证页面(无侧边栏,blank 布局):
| 路由 | 菜单名 | 功能 |
|---|---|---|
/auth/sign-in · /auth/sign-in-2 |
- | Cognito 邮箱密码登录(2 个 variant 待 cleanup) |
/auth/sign-up |
- | 新用户注册 |
/auth/otp · /auth/forgot-password |
- | OTP + 忘记密码流程 |
/auth/ringcentral-callback · /auth/callback · /auth/auth-callback |
- | RingCentral / Cognito OAuth 回调 |
侧边栏 Operational 分组(default 布局):
| 路由 | 菜单名 | 功能 |
|---|---|---|
/dashboard |
Dashboard | 指标卡、图表、排行榜、转化漏斗 |
/activity-timeline |
Activity Timeline | 店铺活动时间线周视图(原 doc 说 /store-activity,实际路径是 /activity-timeline) |
/staff-timeline |
Staff Timeline | 员工维度通话详情 + 周排班视图 |
/calls |
Call Table | 通话分页数据表 + 桌面端并排详情面板 |
/call/:telephonySessionId |
(子页面) | UI Manifest 驱动的动态布局、音频播放、文本同步 |
/messages |
Messages | SMS / 语音邮件 / 传真,按对话分组 |
/tasks |
Tasks | Lead pipeline、任务 CRUD、import、通信日志(16 sub-components) |
/calendar |
Calendar | 店铺排班日历(9 sub-components,逐步替代 /store-schedule) |
/lead-tracker |
Lead Tracker | 线索列表、结果编辑、通话关联 |
/phone-activity |
— | 按号码展示通话/SMS/语音邮件/线索历史 |
侧边栏 Settings 分组(default 布局):
| 路由 | 菜单名 | 功能 |
|---|---|---|
/stores · /stores-management |
Stores & Groups | RingCentral 连接、电话号码分配(2 个路径待统一) |
/store-schedule |
Store Schedule | 店铺周时间表(被 /calendar 替代中) |
/access-management |
Access Management | 团队成员角色分配 |
/settings · /settings/connections · /settings/change-password |
Settings | 账号设置多页面 |
/connections |
Connections | 顶层 connections 页(区别于 /settings/connections) |
/billing · /billing-history · /billing-plan · /transactions |
Billing | 计费、历史、交易 |
/apps |
Apps | 应用卡片 / 集成 |
系统页面:
| 路由 | 功能 |
|---|---|
/errors/401 · /403 · /404 · /500 · /503 |
错误页 |
/legal |
Terms / Privacy 占位 |
/dev/call-detail-test |
开发测试 fixture |
Disabled / WIP(仅 .disabled stub,未启用):
| 路径 | 说明 |
|---|---|
src/pages/users/index.vue.disabled |
Users / invite / roles 页 — 开发中,与 /access-management 可能重叠 |
src/pages/help-center.vue.disabled |
Help center 页 — stub |
Cleanup 候选: activity-timeline-old/ 与 lead-tracker-old/ 目录仍存在,未启用。
3.3 第三层:组件层(~450 Vue/TS 文件)¶
组件层分为两个子层,业务组件组合调用原子组件来构建功能。数字基于 2026-04-23 find src/components -type f \( -name "*.vue" -o -name "*.ts" \) | wc -l 实查。
页面(views/pages)
└── 业务组件(~170 文件)—— 实现具体业务功能
└── 原子组件(shadcn-vue · ~280 文件 · 45 subfolders)—— 通用 UI 积木,被所有业务组件共享
未列出的 doc-silent 目录:access-management/、auth/、call-display/、command-menu-panel/、command-palette/、custom-theme/、dialogs/、dynamic-layout/、empty-state/、global-layout/、inspira-ui/、legal/、marketing/、marketing-layout/、pagination/、settings/、snapshot/、sort-select/、CallAnalysisManifest.vue、ErrorBoundary.vue 等。
3.3.1 上层:业务组件(~170 文件)¶
业务组件之间有交叉调用关系,不能简单按功能一一对应:
| 目录 | 文件数 | 职责 | 调用的其他组件 |
|---|---|---|---|
| analytics/ + dashboard/ | 44 | 指标卡、图表、漏斗、排行榜、热力图 | ui/Card、ui/Tabs、ui/Skeleton |
| call-analysis/ | 7 | UI Manifest 驱动的通话分析(指标网格、教练建议、告警) | ui/Card、ui/Badge、ui/Tabs |
| call-detail/ | 4 | 音频播放器、转录时间线、说话人标注 | ui/Button、ui/Slider、ui/ScrollArea |
| activity-timeline/ | 7 | 周视图热力图、活动聚类、侧抽屉详情 | ui/Drawer、ui/Card、call-detail/、phone-activity/ |
| phone-activity/ | 9 | 按号码展示通话/SMS/语音邮件/线索历史 | ui/Card、ui/Badge、data-table/ |
| data-table/ | 4 | 可排序列头、分面筛选、分页、列管理 | ui/Button、ui/Popover、ui/DropdownMenu |
| filters/ | 2 | 筛选器面板 | ui/Select、ui/Popover、ui/Checkbox |
| stores/ + store-schedule/ | 9 | 店铺配置向导、排班管理、SLA黑名单 | ui/Dialog、ui/Stepper、ui/Form、ui/Calendar |
| conversation-timeline/ | — | SMS 对话线程展示 | ui/ScrollArea、ui/Avatar |
| app-sidebar/ | 4 | 侧边栏框架、店铺切换器 | ui/Sidebar、ui/Select、ui/Avatar |
3.3.2 下层:原子组件(shadcn-vue · 227 文件)¶
来自 shadcn-vue 的通用 UI 积木,不包含任何业务逻辑,被所有业务组件共享复用:
| 分类 | 组件 | 文件数 |
|---|---|---|
| 表单输入 | Input、Textarea、Checkbox、Switch、Select、Pin-input、Tags-input、Calendar、Range-calendar | ~50 |
| 弹出层 | Dialog、Drawer、Sheet、Popover、Hover-card、Dropdown-menu | ~35 |
| 数据展示 | Table、Card、Badge、Avatar、Skeleton、Progress、Stepper | ~40 |
| 导航交互 | Tabs、Button、Accordion、Breadcrumb、Command、Toggle、Carousel | ~55 |
| 反馈通知 | Alert、Sonner(Toast)、Form(校验) | ~15 |
| 布局容器 | ScrollArea、Separator、Collapsible、Sidebar | ~30 |
3.4 第四层:逻辑层¶
3.4.1 TanStack Query Composables(数据查询)¶
位于 src/composables/queries/ — 共 20 个 hook 文件(2026-04-23 ls 实查)。另有 ~35 个非 query composable 分散在 src/composables/ 根,处理 UI 状态(如 useCalendar、useDashboardMetrics、useStaffPerformance),不走 TanStack 缓存。
| Composable | 缓存时间 | 用途 |
|---|---|---|
useCalls(storeId, dateRange) |
3 分钟 | 通话列表 |
useCallDetail(sessionId) |
30 天 | 通话详情(不可变) |
useRecentCalls() |
— | 最近通话 |
useLinkedCall() |
— | 关联通话 |
useAudioUrl() |
— | 录音预签 URL |
useLeads(params) |
5 分钟 | 线索列表 |
useConversations(params) |
10 分钟 | 对话列表(匹配旧 conversationsCache TTL) |
useConversationStatus() |
2 分钟 | 对话状态(同文件) |
useMessagesStore() |
— | 消息 store 查询 |
usePhoneNumbers() |
5 分钟 | 电话号码列表 |
usePhoneActivity() |
— | 按号码历史 |
useStores() |
5 分钟 | 店铺列表 |
useTypedStores() |
— | 类型化店铺查询 |
useOrganizations() |
— | 组织列表(多租户) |
useConnections() |
5 分钟 | RC 连接状态 |
useOAuth() |
— | OAuth 状态 |
useWebhook()(即旧 useWebhookStatus) |
30 秒 | Webhook 实时状态 |
useBlackouts() |
— | 禁呼时段 |
useOperatingHours() |
— | 营业时间 |
useTasks() |
— | 任务列表 |
useDashboardAnalytics() |
— | Dashboard 分析数据 |
useStaffTimelineAnalytics() |
— | 员工时间线分析 |
("—" = 该 hook 未显式设置 staleTime,使用 TanStack Query 默认值 0。)
3.4.2 Pinia Stores(全局状态)¶
pinia-plugin-persistedstate 默认 storage 在 src/plugins/pinia/index.ts:9 设为 sessionStorage(tab 生命周期内持久)。只有 auth store 直接用 localStorage.* API 手动持久,绕开了 plugin。
| Store | 持久化 | 关键状态 |
|---|---|---|
| auth | localStorage(手动 localStorage.setItem) |
idToken、accessToken、user、memberships |
| stores | 否 | stores[]、currentStoreId |
| organization | sessionStorage(persist: true) |
当前 org、多租户切换(核心多租户开关) |
| ringcentral | sessionStorage(手动 sessionStorage.setItem) |
isConnected、phoneNumbers、OAuth state |
| date-range | 否 | startDate、endDate(URL 已是持久载体) |
theme (system-config) |
sessionStorage(persist: true) |
radius: number + theme: Theme(8 种色名枚举 zinc / red / rose / orange / green / blue / yellow / violet) |
| access-management | sessionStorage | Access management 页状态 |
| analytics-dashboard | sessionStorage | Dashboard 筛选、tab 状态 |
| calls-table | sessionStorage | Call table 列配置、排序 |
| lead-tracker-table | sessionStorage | Lead tracker 列配置、排序 |
3.5 第五层:API 层¶
Axios 实例(src/api/index.ts)
├── 请求拦截器:自动附加 Authorization: Bearer {token} + Sentry breadcrumb
├── 响应拦截器:401 → 自动刷新令牌(最多重试 3 次);其他错误 → Sentry 捕获 + Toast
├── OT API 模块(src/api/ot-api/,共 15 个文件)
│ ├── auth.ts / base.ts / index.ts(底座)
│ ├── calls.ts · conversations.ts · messages.ts · messages-store.ts · webhook.ts(通话 + 消息)
│ ├── leads.ts · tasks.ts · phone-activity.ts · phone-numbers.ts(线索 + 活动)
│ ├── stores.ts · connections.ts · blackouts.ts · operating-hours.ts(店铺 + 配置)
└── 并行 Typed API 层(Hono RPC)
├── src/api/hono-client.ts → Hono 类型化 HTTP client
├── src/api/typed-api.ts → 基于 schema 的类型化查询
├── src/api/ot-api-schemas.ts → Zod schema 定义
├── src/api/analytics-14day-api.ts / calls-client.ts / tasks-client.ts
├── src/api/ringcentral-api.ts → 直调 RC API(OAuth flow)
└── src/api/sub-accounts-api.ts → Sub-account 操作
为什么有两套? 大部分域走 Axios + src/api/ot-api/ 惯用模式。新域(analytics、tasks、typed routes)试点 Hono RPC 获得编译期类型安全。迁移策略尚未 finalize。
4. 认证流程¶
用户访问 → 路由守卫检查 authStore.isLogin
├── 未登录 → 重定向 /auth/sign-in
│ → 输入邮箱密码 → AWS Cognito 验证
│ → 获取 JWT(idToken + accessToken + refreshToken)
│ → 存入 localStorage → 加载用户成员资格
│ → 初始化 RingCentral Store → 重定向 /dashboard
└── 已登录 → 继续访问目标页面
→ API 请求自动携带令牌
→ 401 时自动刷新 → 失败则登出
5. 数据获取模式¶
所有页面数据通过 TanStack Query 管理,保证缓存一致性:
页面组件调用 useCalls({ storeId, dateRange })
↓
TanStack Query 检查缓存
├── 缓存命中且未过期 → 直接返回
└── 缓存不存在或已过期 → 发起请求
↓
Axios → 请求拦截器附加令牌 → studio-api
↓
响应数据写入缓存 → Vue 反应性更新 → 页面重新渲染
缓存失效策略:
- 时间失效:根据数据类型设置不同的 staleTime(30秒 ~ 30天)
- 手动失效:mutation 成功后调用
queryClient.invalidateQueries() - 响应式 Key:
storeId或dateRange变化时自动重新获取
6. 技术栈¶
| 层级 | 技术 |
|---|---|
| 框架 | Vue 3.5 + TypeScript |
| 构建 | Vite 8 (beta)(rolldown-ready;当前仍是 Rollup,未 swap 到 rolldown-vite) |
| 样式 | Tailwind CSS v4 + shadcn-vue + Reka UI |
| 路由 | Vue Router + unplugin-vue-router(文件路由)+ vite-plugin-vue-layouts(blank / default / analysis 布局) |
| 状态 | Pinia + pinia-plugin-persistedstate(默认 sessionStorage) |
| 数据获取 | TanStack Query + Axios + Hono RPC(typed API 试点) |
| 图表 | ECharts + vue-echarts + Unovis |
| 表单 | Vee-Validate + Zod |
| 国际化 | vue-i18n 11.x |
| 认证 | AWS Amplify(Cognito) |
| 图标 | Lucide Vue |
| 动画 | motion-v + AutoAnimate |
| 工具 | VueUse、Day.js |
| 共享类型 | @retaintive/common ^0.3.0(⚠️ 后端 infra 当前 ^0.18.0,前端落后 ~15 个 minor;使用方需确认是否需要 bump) |
| 测试 | Vitest + Vue Test Utils + Puppeteer + Lighthouse(perf budget) |
| 代码健康 | Oxlint + Knip(未使用依赖扫描) |
| 部署 | Cloudflare Pages(Wrangler),通过 @sentry/vite-plugin 在 prod build 上传 sourcemap |
| 监控 | Sentry + Google Analytics 4 |
7. 部署环境¶
前端代码位于 studio-website-monorepo 的 apps/web 目录,通过 GitHub Actions 部署到 Cloudflare Pages。共 3 个环境(对应 3 个 workflow 文件:deploy-test.yml / deploy-pre.yml / deploy-prod.yml)。
| 环境 | 触发方式 | 域名 |
|---|---|---|
| Test | push 到 main 时自动部署 |
studio-test.retaintive.ai |
| Pre | push 到 release/pre 分支 或 workflow_dispatch |
studio-pre Cloudflare project(自托管 runner) |
| Production | workflow_dispatch 手动触发(需输入 prod 确认) |
studio.retaintive.ai |
Legacy retaintive/studio-web repo
github.com/retaintive/studio-web 是 pre-monorepo 版本的前端仓库,已不再活跃(最后 commit 2026-03-04)。所有新工作走 studio-website-monorepo/apps/web。如果误落到 studio-web,请与团队确认是否待 archive。