diff --git a/README.md b/README.md
index 81dd857..a4e69a5 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,7 @@ src/
api.ts 前后端共享 TypeScript 类型
web/ Vite + React 前端 Dashboard
components/ UI 组件(卡片、分组、模态框、状态条等)
+ constants/ 常量定义(类型映射等)
hooks/ 数据轮询和详情管理 hooks
utils/ 前端工具函数
scripts/ 开发、构建和 smoke test 脚本
diff --git a/openspec/specs/card-dashboard/spec.md b/openspec/specs/card-dashboard/spec.md
index 745b04c..a5f00d0 100644
--- a/openspec/specs/card-dashboard/spec.md
+++ b/openspec/specs/card-dashboard/spec.md
@@ -11,9 +11,13 @@ Dashboard SHALL 按分组展示所有拨测目标,每个分组包含带统计
- **WHEN** 用户打开 Dashboard 页面
- **THEN** 页面 SHALL 按分组展示目标卡片,"默认分组" 排在最上面,其余分组按 YAML 配置顺序排列
-#### Scenario: 分组标题带统计
+#### Scenario: 分组标题带统计徽章
- **WHEN** 页面渲染某个分组
-- **THEN** 分组标题 SHALL 显示分组名称、该分组内目标总数、正常数和异常数,格式为 `分组名 (N个, X UP / Y DOWN)`
+- **THEN** 分组标题 SHALL 显示分组名称和三个徽章:总数(蓝色)、正常数(绿色)、异常数(红色),徽章仅显示数字
+
+#### Scenario: 分组统计徽章提示
+- **WHEN** 鼠标悬停在分组统计徽章上
+- **THEN** 徽章 SHALL 显示提示文字("总数"、"正常"、"异常")
#### Scenario: "default" 分组显示名称
- **WHEN** 分组名称为 "default"
@@ -28,7 +32,7 @@ Dashboard SHALL 使用固定宽度的卡片配合 Flexbox 流动布局,容器
#### Scenario: 卡片固定宽度
- **WHEN** 页面渲染卡片(包括 Summary Cards 和 Target Cards)
-- **THEN** 每个卡片 SHALL 固定宽度 270px,使用 `flex-shrink: 0` 防止收缩
+- **THEN** 每个卡片 SHALL 固定宽度 280px,使用 CSS 变量 `--dashboard-card-width` 统一控制
#### Scenario: 流动式布局
- **WHEN** 视口宽度变化
@@ -47,7 +51,11 @@ Dashboard SHALL 使用固定宽度的卡片配合 Flexbox 流动布局,容器
#### Scenario: 卡片第一层内容
- **WHEN** 卡片渲染
-- **THEN** 卡片第一层 SHALL 展示状态指示圆点(UP 绿色 / DOWN 红色)、目标名称和类型标签(HTTP / Command)
+- **THEN** 卡片第一层 SHALL 展示状态指示圆点(UP 绿色 / DOWN 红色)、目标名称和类型标签(HTTP / CMD)
+
+#### Scenario: 卡片名称完整提示
+- **WHEN** 目标名称过长被截断显示
+- **THEN** 鼠标悬停在名称上 SHALL 通过浏览器原生 tooltip 显示完整名称
#### Scenario: 卡片状态指示圆点
- **WHEN** 目标最近一次拨测 matched=true
diff --git a/openspec/specs/target-detail-modal/spec.md b/openspec/specs/target-detail-modal/spec.md
index 166b237..a535016 100644
--- a/openspec/specs/target-detail-modal/spec.md
+++ b/openspec/specs/target-detail-modal/spec.md
@@ -74,3 +74,14 @@ Dashboard SHALL 在用户点击目标卡片后弹出模态框,展示该目标
#### Scenario: 自上而下渲染
- **WHEN** 模态框渲染
- **THEN** 内容区域 SHALL 分为上下两部分,上方展示统计图表,下方展示检查结果列表和分页器
+
+### Requirement: 模态框标题栏类型标签
+模态框标题栏 SHALL 显示目标类型标签,使用统一的类型显示映射系统。
+
+#### Scenario: 类型标签显示
+- **WHEN** 模态框标题栏渲染
+- **THEN** 标题栏 SHALL 在目标名称旁显示类型标签(HTTP / CMD)
+
+#### Scenario: 类型标签使用映射系统
+- **WHEN** 模态框渲染类型标签
+- **THEN** 类型标签 SHALL 使用统一的类型显示映射函数,不硬编码映射逻辑
diff --git a/openspec/specs/target-type-display/spec.md b/openspec/specs/target-type-display/spec.md
new file mode 100644
index 0000000..2f61ec8
--- /dev/null
+++ b/openspec/specs/target-type-display/spec.md
@@ -0,0 +1,42 @@
+## Purpose
+
+定义目标类型(Target Type)的前端显示名称映射系统,支持从后端类型标识符到前端展示名称的可扩展转换。
+
+## Requirements
+
+### Requirement: 类型显示名称映射
+系统 SHALL 提供目标类型到显示名称的映射,将后端类型标识符转换为前端展示的简短名称。
+
+#### Scenario: HTTP 类型显示
+- **WHEN** 目标类型为 "http"
+- **THEN** 前端 SHALL 显示 "HTTP"
+
+#### Scenario: Command 类型显示
+- **WHEN** 目标类型为 "command"
+- **THEN** 前端 SHALL 显示 "CMD"
+
+#### Scenario: 未知类型处理
+- **WHEN** 目标类型不在映射表中
+- **THEN** 前端 SHALL 将类型名称转换为大写显示
+
+### Requirement: 映射可扩展性
+类型映射系统 SHALL 支持后续新增类型,无需修改多处代码。
+
+#### Scenario: 新增类型映射
+- **WHEN** 需要新增目标类型(如 "tcp"、"dns"、"grpc")
+- **THEN** 开发者 SHALL 仅需在映射常量中添加一条记录
+
+#### Scenario: 映射单一数据源
+- **WHEN** 前端组件需要显示目标类型
+- **THEN** 组件 SHALL 调用统一的映射函数,不直接硬编码映射逻辑
+
+### Requirement: 类型安全
+类型映射系统 SHALL 提供类型安全的访问方式。
+
+#### Scenario: TypeScript 类型推导
+- **WHEN** 使用映射常量
+- **THEN** TypeScript SHALL 能够推导出正确的类型(使用 `as const`)
+
+#### Scenario: 运行时安全
+- **WHEN** 传入无效类型
+- **THEN** 系统 SHALL 返回 fallback 值,不抛出异常
diff --git a/src/web/components/GroupHeader.tsx b/src/web/components/GroupHeader.tsx
index 25f4be8..b10fc5d 100644
--- a/src/web/components/GroupHeader.tsx
+++ b/src/web/components/GroupHeader.tsx
@@ -11,9 +11,17 @@ export function GroupHeader({ name, total, up, down }: GroupHeaderProps) {
return (
{displayName}
-
- ({total}个, {up} UP / {down} DOWN)
-
+
+
+ {total}
+
+
+ {up}
+
+
+ {down}
+
+
);
}
diff --git a/src/web/components/TargetCard.tsx b/src/web/components/TargetCard.tsx
index 3a8d9d6..8a12273 100644
--- a/src/web/components/TargetCard.tsx
+++ b/src/web/components/TargetCard.tsx
@@ -2,6 +2,7 @@ import type { TargetStatus } from "../../shared/api";
import { StatusDot } from "./StatusDot";
import { StatusBar } from "./StatusBar";
import { MiniSparkline } from "./MiniSparkline";
+import { getTargetTypeDisplay } from "../constants/target-type-display";
interface TargetCardProps {
target: TargetStatus;
@@ -15,8 +16,10 @@ export function TargetCard({ target, onClick }: TargetCardProps) {
- {target.name}
- {target.type === "http" ? "HTTP" : "Command"}
+
+ {target.name}
+
+ {getTargetTypeDisplay(target.type)}
diff --git a/src/web/components/TargetDetailModal.tsx b/src/web/components/TargetDetailModal.tsx
index 2a87886..a5de5c7 100644
--- a/src/web/components/TargetDetailModal.tsx
+++ b/src/web/components/TargetDetailModal.tsx
@@ -4,6 +4,7 @@ import { StatusDonut } from "./StatusDonut";
import { StatusDot } from "./StatusDot";
import { TimeRangePicker } from "./TimeRangePicker";
import { Pagination } from "./Pagination";
+import { getTargetTypeDisplay } from "../constants/target-type-display";
interface TargetDetailModalProps {
target: TargetStatus;
@@ -48,7 +49,7 @@ export function TargetDetailModal({
{target.name}
- {target.type === "http" ? "HTTP" : "Command"}
+ {getTargetTypeDisplay(target.type)}