Files
Edu/apps/teacher-portal/src/lib/observability/web-vitals.ts
SpecialX d49d211425 feat(teacher-portal): 完成参考项目差距闭环 P3-P7 全量实现
- P3 考试/作业/成绩 mutation + 详情页 + 批改界面 + 乐观更新 + 多 Tab 同步
- P4 知识图谱 SVG 可视化 + 学情分析仪表盘 + parent-portal Remote
- P5 WebSocket 通知中心 + AI 出题(SSE) + AI 教案 + AI 学情报告
- P6 可观测性硬化:Sentry + WebVitals + OTel + A11y + 性能配置 + Cookie 迁移
- P7 参考项目差距闭环:新增 35 个页面覆盖 13 个缺失模块
  - attendance(考勤 4 页)/questions(题库)/textbooks(教材 2 页)
  - classes/[id] 详情 + classes/schedule 课表
  - course-plans(2 页)/diagnostic(2 页)/error-book/practice
  - exams/[id]/build 组卷 + exams/[id]/analytics 考后分析
  - exams/[id]/edit-rich 富文本编辑 + exams/[id]/proctoring 监考
  - grades/entry 批量录入 + grades/stats 统计 + grades/analytics 分析 + grades/report-card 报告卡
  - homework/submissions 列表 + assignments/[id]/submissions 批量批改
  - homework/submissions/[submissionId] 单份批改 + scan-grading 扫描批改
  - lesson-plans 编辑器 + library + calendar + heatmap 5 页
  - elective 选修课 3 页 /leave 请假 /schedule-changes 调课
- P7 基础设施:61 GraphQL operations + 5 handlers + 13 fixtures + 11 viewports
- 集成 browser.ts/server.ts 注册所有 p7 handlers(fallthrough 顺序)
- viewports.ts 扩展 11 个新导航项
- 验证:tsc --noEmit 零错误 + eslint 零错误
- 文档:workline.md 新增 §5 P7 参考项目差距闭环(含完整文件清单)
2026-07-13 14:27:04 +08:00

132 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Web Vitals RUM 采集P6 硬化)
*
* - 参考 student-portal 和 admin-portal 的实现
* - 使用 web-vitals 库的 onCLS / onINP / onLCP / onTTFB / onFCP
* - 将指标上报到 /api/v1/admin/web-vitalsnavigator.sendBeacon
* - 导出 initWebVitals() + WebVitalMetric 类型
*
* 关联02-architecture-design.md §12 可观测性
*/
import { getObservabilityConfig, isWebVitalsReportingEnabled } from "@/lib/observability/env";
/**
* Web Vital 指标结构(与 web-vitals 库 Metric 对齐)。
*/
export interface WebVitalMetric {
/** 指标名LCP / CLS / INP / FCP / TTFB */
name: string;
/** 指标值 */
value: number;
/** 评级good / needs-improvement / poor */
rating: string;
/** 指标唯一标识 */
id: string;
/** 增量值(部分指标使用) */
delta?: number;
}
/**
* web-vitals 库的回调函数类型(最小声明)。
* 避免对未安装包的静态类型依赖。
*/
type MetricCallback = (metric: WebVitalMetric) => void;
interface WebVitalsLib {
onLCP(cb: MetricCallback): void;
onCLS(cb: MetricCallback): void;
onFCP(cb: MetricCallback): void;
onINP(cb: MetricCallback): void;
onTTFB(cb: MetricCallback): void;
}
/** 上报状态标记,避免重复注册回调 */
let initialized = false;
/**
* 上报单个 Web Vital 指标。
*
* 使用 navigator.sendBeacon 优先(页面卸载时不丢失),
* 降级到 fetch keepalive。
*/
function sendMetric(metric: WebVitalMetric): void {
if (typeof window === "undefined") return;
if (!isWebVitalsReportingEnabled()) return;
const config = getObservabilityConfig();
const payload = {
name: metric.name,
value: metric.value,
rating: metric.rating,
id: metric.id,
delta: metric.delta,
service: config.serviceName,
portal: "teacher",
page: window.location.pathname,
timestamp: Date.now(),
};
try {
const body = JSON.stringify(payload);
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
const blob = new Blob([body], { type: "application/json" });
navigator.sendBeacon(config.webVitalsEndpoint, blob);
return;
}
// sendBeacon 不可用时降级 fetch keepalive
void fetch(config.webVitalsEndpoint, {
method: "POST",
body,
headers: { "Content-Type": "application/json" },
keepalive: true,
}).catch(() => {
// 上报失败不影响用户体验,静默忽略
});
} catch {
// 上报失败静默忽略
}
}
/**
* 初始化 Web Vitals 采集。
*
* 使用动态 import 加载 web-vitals 库,避免未安装包导致构建失败。
* 包将在统一安装阶段补充到 package.json。
*/
export async function initWebVitals(): Promise<void> {
if (initialized) return;
if (typeof window === "undefined") return;
try {
const webVitals = (await import("web-vitals")) as unknown as WebVitalsLib;
webVitals.onLCP(sendMetric);
webVitals.onCLS(sendMetric);
webVitals.onFCP(sendMetric);
webVitals.onINP(sendMetric);
webVitals.onTTFB(sendMetric);
initialized = true;
} catch (err) {
// web-vitals 未安装或加载失败,降级到无采集
if (typeof console !== "undefined") {
console.warn("[teacher-portal] web-vitals 加载失败", err);
}
}
}
/**
* 手动上报单个 Web Vital 指标(供 Next.js reportWebVitals 使用)。
*
* 用法:在 layout.tsx 中 export function reportWebVitals(metric) { sendWebVital(metric); }
*/
export function reportWebVitals(metric: WebVitalMetric): void {
sendMetric(metric);
}
/** Web Vitals 是否已初始化 */
export function isWebVitalsInitialized(): boolean {
return initialized;
}