metricRuntime.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import { metricsData, type MetricCategoryData, type MetricValue, type SubMetric } from "@/components/metricsData";
  2. import type { D4MetricsResponse } from "@/lib/d4Metrics";
  3. export type MetricDataState = "ready" | "loading" | "error" | "unavailable";
  4. export type RuntimeMetricValue = Omit<MetricValue, "score"> & {
  5. displayValue: string;
  6. helperText?: string;
  7. progress: number | null;
  8. score: number | null;
  9. state: MetricDataState;
  10. };
  11. export type RuntimeSubMetric = Omit<SubMetric, "values"> & {
  12. values: RuntimeMetricValue[];
  13. score: number | null;
  14. state: MetricDataState;
  15. };
  16. export type RuntimeMetric = Omit<MetricCategoryData, "subMetrics"> & {
  17. subMetrics: RuntimeSubMetric[];
  18. avgScore: string;
  19. score: number | null;
  20. state: MetricDataState;
  21. summary: string;
  22. };
  23. export type RuntimeMetricsByCategory = Record<string, RuntimeMetric[]>;
  24. export interface D4MetricsLoadState {
  25. data: D4MetricsResponse | null;
  26. isLoading: boolean;
  27. error: string | null;
  28. }
  29. const countFormatter = new Intl.NumberFormat("zh-CN");
  30. function clampScore(value: number) {
  31. return Math.max(0, Math.min(100, Math.round(value)));
  32. }
  33. function rateToScore(rate: number | null | undefined) {
  34. return rate == null ? null : clampScore(rate * 100);
  35. }
  36. function averageScore(values: Array<number | null | undefined>) {
  37. const validValues = values.filter((value): value is number => typeof value === "number" && !Number.isNaN(value));
  38. if (validValues.length === 0) {
  39. return null;
  40. }
  41. return clampScore(validValues.reduce((sum, value) => sum + value, 0) / validValues.length);
  42. }
  43. function formatPercent(rate: number | null | undefined) {
  44. const score = rateToScore(rate);
  45. return score == null ? "暂无数据" : `${score}%`;
  46. }
  47. function formatCount(value: number | null | undefined) {
  48. return value == null ? "暂无数据" : countFormatter.format(value);
  49. }
  50. function formatDuration(ms: number | null | undefined) {
  51. if (ms == null) {
  52. return "暂无数据";
  53. }
  54. const totalSeconds = Math.max(0, Math.round(ms / 1000));
  55. if (totalSeconds < 60) {
  56. return `${totalSeconds} 秒`;
  57. }
  58. const totalMinutes = Math.round(totalSeconds / 60);
  59. if (totalMinutes < 60) {
  60. return `${totalMinutes} 分钟`;
  61. }
  62. const hours = Math.floor(totalMinutes / 60);
  63. const minutes = totalMinutes % 60;
  64. if (hours < 24) {
  65. return minutes > 0 ? `${hours} 小时 ${minutes} 分钟` : `${hours} 小时`;
  66. }
  67. const days = Math.floor(hours / 24);
  68. const restHours = hours % 24;
  69. return restHours > 0 ? `${days} 天 ${restHours} 小时` : `${days} 天`;
  70. }
  71. function buildPendingValue(value: MetricValue, state: MetricDataState, message: string, helperText?: string): RuntimeMetricValue {
  72. return {
  73. ...value,
  74. displayValue: message,
  75. helperText,
  76. progress: null,
  77. score: null,
  78. state,
  79. };
  80. }
  81. function buildPendingMetric(metric: MetricCategoryData, state: MetricDataState, message: string, summary: string): RuntimeMetric {
  82. return {
  83. ...metric,
  84. avgScore: state === "loading" ? "..." : "--",
  85. score: null,
  86. state,
  87. summary,
  88. subMetrics: metric.subMetrics.map((subMetric) => ({
  89. ...subMetric,
  90. score: null,
  91. state,
  92. values: subMetric.values.map((value) => buildPendingValue(value, state, message, summary)),
  93. })),
  94. };
  95. }
  96. function buildD4ReadyMetric(metric: MetricCategoryData, data: D4MetricsResponse): RuntimeMetric {
  97. const completionScore = rateToScore(data.taskCompletion.completionRate);
  98. const onTimeScore = rateToScore(data.taskCompletion.onTimeCompletionRate);
  99. const conflictScore = rateToScore(data.conflict.resolutionRate);
  100. const score = averageScore([completionScore, onTimeScore, conflictScore]);
  101. const subMetrics: RuntimeSubMetric[] = metric.subMetrics.map((subMetric) => {
  102. if (subMetric.id === "D4-1") {
  103. const values: RuntimeMetricValue[] = [
  104. {
  105. name: "协作任务完成率",
  106. displayValue: formatPercent(data.taskCompletion.completionRate),
  107. helperText: `${formatCount(data.taskCompletion.completedCount)} / ${formatCount(data.taskCompletion.assignedCount)} 项任务完成`,
  108. progress: completionScore,
  109. score: completionScore,
  110. state: "ready",
  111. },
  112. {
  113. name: "个人任务按时完成率",
  114. displayValue: formatPercent(data.taskCompletion.onTimeCompletionRate),
  115. helperText: `${formatCount(data.taskCompletion.onTimeCompletedCount)} / ${formatCount(data.taskCompletion.completedCount)} 项完成任务满足 SLA`,
  116. progress: onTimeScore,
  117. score: onTimeScore,
  118. state: "ready",
  119. },
  120. ];
  121. return {
  122. ...subMetric,
  123. values,
  124. score: averageScore(values.map((value) => value.score)),
  125. state: "ready",
  126. };
  127. }
  128. if (subMetric.id === "D4-2") {
  129. const values: RuntimeMetricValue[] = [
  130. {
  131. name: "团队沟通平均响应耗时",
  132. displayValue: formatDuration(data.communication.averageResponseMs),
  133. helperText: `${formatCount(data.communication.respondedMessageCount)} / ${formatCount(data.communication.sentMessageCount)} 条消息形成响应`,
  134. progress: null,
  135. score: null,
  136. state: "ready",
  137. },
  138. {
  139. name: "有效协作次数",
  140. displayValue: formatCount(data.collaboration.effectiveCollaborationCount),
  141. helperText: "D4_COLLAB_EFFECTIVE 事件计数",
  142. progress: null,
  143. score: null,
  144. state: "ready",
  145. },
  146. {
  147. name: "协作冲突解决次数",
  148. displayValue: formatCount(data.conflict.resolvedCount),
  149. helperText: `${formatCount(data.conflict.createdCount)} 次冲突创建`,
  150. progress: null,
  151. score: null,
  152. state: "ready",
  153. },
  154. {
  155. name: "协作冲突解决率",
  156. displayValue: formatPercent(data.conflict.resolutionRate),
  157. helperText: `${formatCount(data.conflict.resolvedCount)} / ${formatCount(data.conflict.createdCount)} 次冲突已解决`,
  158. progress: conflictScore,
  159. score: conflictScore,
  160. state: "ready",
  161. },
  162. ];
  163. return {
  164. ...subMetric,
  165. values,
  166. score: averageScore(values.map((value) => value.score)),
  167. state: "ready",
  168. };
  169. }
  170. return {
  171. ...subMetric,
  172. score: null,
  173. state: "ready",
  174. values: subMetric.values.map((value) => buildPendingValue(value, "ready", "暂无数据")),
  175. };
  176. });
  177. return {
  178. ...metric,
  179. avgScore: score == null ? "--" : `${score}`,
  180. score,
  181. state: "ready",
  182. summary: `已同步 ${formatCount(data.scan.scannedEvents)} 条 D4 日志事件`,
  183. subMetrics,
  184. };
  185. }
  186. function buildD4Metric(metric: MetricCategoryData, loadState: D4MetricsLoadState): RuntimeMetric {
  187. if (loadState.isLoading) {
  188. return buildPendingMetric(metric, "loading", "加载中", "正在从 D4 指标接口同步数据");
  189. }
  190. if (loadState.error) {
  191. return buildPendingMetric(metric, "error", "接口异常", loadState.error);
  192. }
  193. if (!loadState.data) {
  194. return buildPendingMetric(metric, "unavailable", "暂无数据", "D4 接口尚未返回数据");
  195. }
  196. return buildD4ReadyMetric(metric, loadState.data);
  197. }
  198. export function buildRuntimeMetrics(d4LoadState: D4MetricsLoadState): RuntimeMetricsByCategory {
  199. return Object.fromEntries(
  200. Object.entries(metricsData).map(([category, metrics]) => [
  201. category,
  202. metrics.map((metric) => {
  203. if (metric.id === "D4") {
  204. return buildD4Metric(metric, d4LoadState);
  205. }
  206. return buildPendingMetric(metric, "unavailable", "未接入", "后端暂未提供该指标接口");
  207. }),
  208. ]),
  209. );
  210. }
  211. export function getRuntimeMetric(metrics: RuntimeMetricsByCategory, metricId: string) {
  212. const normalizedMetricId = metricId.toUpperCase();
  213. const category = normalizedMetricId.charAt(0);
  214. return metrics[category]?.find((metric) => metric.id === normalizedMetricId) ?? null;
  215. }