- {!this.canEditName ? (
+ {this.metricData.is_family_parent ? (
+
{
{this.$t('单位')}:
- {!this.canEditUnit ? (
+ {!this.canEditUnit && !this.metricData.is_family_parent ? (
this.handleShowEditUnit(this.metricData.config.unit)}
@@ -532,43 +621,85 @@ export default class MetricDetail extends tsc {
{this.metricData.config.unit || '--'}
) : (
-
this.handleEditUnit(v, this.metricData)}
- >
- {this.unitList.map((group, index) => (
-
- {group.formats.map(option => (
-
- ))}
-
- ))}
-
+ this.canEditUnit ? (
+
this.handleEditUnit(v, this.metricData)}
+ >
+ {this.unitList.map((group, index) => (
+
+ {group.formats.map(option => (
+
+ ))}
+
+ ))}
+
+ ) : (
+
{this.metricData.config.unit || '--'}
+ )
)}
+
+
{this.$t('类型')}:
+
+
+
+ {this.metricData.type_source === 'manual' ? this.$t('人工确认') : this.$t('自动识别')}
+
+
+
+ {this.showTemporality && (
+
+
{this.$t('时序性')}:
+ {!this.canEditTemporality ? (
+
+ {this.temporalityLabel}
+
+ ) : (
+
this.handleEditTemporality(v)}
+ >
+ {METRIC_TEMPORALITY_OPTIONS.map(item => (
+
+ ))}
+
+ )}
+
+ )}
{/* 汇聚方法 */}
{this.$t('汇聚方法')}:
- {!this.canEditAgg ? (
+ {!this.canEditAgg && !this.metricData.is_family_parent ? (
this.handleShowEditAgg(this.metricData.config.aggregate_method)}
>
{this.metricData.config.aggregate_method || '--'}
- ) : (
+ ) : this.canEditAgg ? (
{
/>
))}
+ ) : (
+
{this.metricData.config.aggregate_method || '--'}
)}
diff --git a/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/components/indicator-dimension/components/metric-list/index.scss b/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/components/indicator-dimension/components/metric-list/index.scss
index c130260c5ac..f2766798595 100644
--- a/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/components/indicator-dimension/components/metric-list/index.scss
+++ b/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/components/indicator-dimension/components/metric-list/index.scss
@@ -131,14 +131,16 @@ $border-color: #d9d9d9;
.bk-table {
overflow: visible;
- .bk-table-body-wrapper {
- border-right: 1px solid #dfe0e5;
- }
-
.bk-table-fixed-body-wrapper {
border-left: 1px solid #dfe0e5;
}
+ // 最右列格子自带 border-right,再叠 wrapper/::after 会变成 2px
+ th.is-last,
+ td.is-last {
+ border-right: none;
+ }
+
th {
&:nth-child(1) {
border-top: 1px solid #dfe0e5;
@@ -201,6 +203,57 @@ $border-color: #d9d9d9;
color: #3a84ff;
}
+ .metric-name-cell {
+ display: flex;
+ align-items: center;
+ min-width: 0;
+ color: #3a84ff;
+ cursor: pointer;
+
+ &.is-family-child {
+ padding-left: 8px;
+
+ .metric-name-text {
+ padding-left: 9px;
+ }
+ }
+
+ .family-expand-icon {
+ margin-right: 4px;
+ font-size: 12px;
+ color: #979ba5;
+ cursor: pointer;
+ transition: transform 0.2s ease;
+
+ &.is-expanded {
+ transform: rotate(90deg);
+ }
+ }
+
+ .family-expand-placeholder {
+ display: inline-block;
+ flex-shrink: 0;
+ width: 16px;
+ }
+
+ .metric-name-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ .type-source-cell {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+
+ .type-conflict-btn {
+ padding: 0;
+ font-size: 12px;
+ }
+ }
+
.cell {
&:has(.table-group-box) {
padding-left: 5px;
@@ -212,6 +265,23 @@ $border-color: #d9d9d9;
}
}
+ // 子指标层级竖线画在 td 上,避免被 .cell overflow 裁切,并铺满行高(42px)
+ .bk-table-row td:has(.is-family-child) {
+ position: relative;
+
+ &::before {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 39px;
+ z-index: 1;
+ width: 1px;
+ pointer-events: none;
+ content: '';
+ background: #dcdee5;
+ }
+ }
+
.table-group-box {
overflow: visible;
@@ -273,6 +343,12 @@ $border-color: #d9d9d9;
}
.description-content {
+ &.is-readonly {
+ padding: 0 10px;
+ color: #63656e;
+ line-height: 32px;
+ }
+
.description-input {
input {
background-color: #fff !important;
@@ -356,7 +432,6 @@ $border-color: #d9d9d9;
.detail {
background: #f5f7fa;
border: 1px solid #dcdee5;
- border-left: none;
.metric-card {
padding: 14px 12px 8px 24px;
diff --git a/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/components/indicator-dimension/components/metric-list/index.tsx b/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/components/indicator-dimension/components/metric-list/index.tsx
index b324a88698a..2d58525a723 100644
--- a/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/components/indicator-dimension/components/metric-list/index.tsx
+++ b/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/components/indicator-dimension/components/metric-list/index.tsx
@@ -34,7 +34,16 @@ import EmptyStatus from '../../../../../../../components/empty-status/empty-stat
import { METHOD_LIST } from '../../../../../../../constant/constant';
import ColumnCheck from '../../../../../../performance/column-check/column-check.vue';
import { type IGroupListItem, type RequestHandlerMap, DEFAULT_HEIGHT_OFFSET, NULL_LABEL } from '../../../../type';
+import {
+ type MetricTypeValue,
+ METRIC_TYPE_ALL,
+ enrichAndCollapseMetricTypeList,
+ ensureDemoMetricFamilyIfNeeded,
+ getMetricTypeSourceLabel,
+ parseMetricFamilyName,
+} from '../../../../metric-type';
import { matchRuleFn } from '../../../../utils';
+import MetricTypeTag from '../../../metric-type-tag';
import BatchEdit from './components/batch-edit';
import MetricDetail from './components/metric-detail';
@@ -46,7 +55,15 @@ import './index.scss';
import '@blueking/search-select-v3/vue2/vue2.css';
/** 指标项类型定义,在基础字段数据基础上增加错误信息、新增标记和选中状态 */
-export type IMetricItem = ICustomTsFields['list'][number] & { error?: string; isNew?: boolean; selection: boolean };
+export type IMetricItem = ICustomTsFields['list'][number] & {
+ error?: string;
+ isNew?: boolean;
+ selection: boolean;
+ expandable?: boolean;
+ __expanded?: boolean;
+ __family_member_rows?: IMetricItem[];
+ __is_family_child?: boolean;
+};
/** 组件事件接口 */
interface IEmits {
@@ -74,6 +91,8 @@ interface IProps {
metricGroupsMap: Map
;
/** 当前选中的分组信息 */
selectedGroupInfo: { id: number; name: string };
+ /** 当前选中的指标类型(与分组 AND) */
+ selectedMetricType: MetricTypeValue | typeof METRIC_TYPE_ALL;
/** 单位列表 */
unitList: IUnitItem[];
/** 时间序列分组 ID */
@@ -87,6 +106,7 @@ interface IProps {
@Component
export default class MetricList extends tsc {
@Prop({ default: () => {} }) selectedGroupInfo: IProps['selectedGroupInfo'];
+ @Prop({ default: METRIC_TYPE_ALL }) selectedMetricType: IProps['selectedMetricType'];
@Prop({ default: () => [] }) unitList: IProps['unitList'];
@Prop({ default: () => [] }) groupSelectList: IProps['groupSelectList'];
@Prop({ default: () => [] }) dimensionTable: IProps['dimensionTable'];
@@ -274,6 +294,14 @@ export default class MetricList extends tsc {
this.handleGetCustomTsFields();
}
+ /** 监听指标类型筛选变化(与分组 AND) */
+ @Watch('selectedMetricType')
+ handleSelectedMetricTypeChange() {
+ this.tableInstance.page = 1;
+ this.selectedMetricMap = {};
+ this.handleGetCustomTsFields();
+ }
+
/** 监听指标数据变化,当数据为空时关闭详情面板 */
@Watch('metricData', { immediate: true, deep: true })
handleMetricDataChange() {
@@ -294,9 +322,21 @@ export default class MetricList extends tsc {
name: {
checked: true,
disable: false,
- name: this.$t('名称'),
+ name: this.$t('指标/指标族'),
id: 'name',
},
+ metricType: {
+ checked: true,
+ disable: false,
+ name: this.$t('类型'),
+ id: 'metric_type',
+ },
+ typeSource: {
+ checked: true,
+ disable: false,
+ name: this.$t('类型来源'),
+ id: 'type_source',
+ },
alias: {
checked: true,
disable: false,
@@ -392,11 +432,20 @@ export default class MetricList extends tsc {
this.requestHandlerMap
.getCustomTsFields(params)
.then(data => {
- this.metricTable = data.list.map(item => ({
- ...item,
- selection: false,
- }));
- this.tableInstance.total = data.total;
+ // 后端字段缺失时 Mock 类型元数据,并将 Histogram/Summary 按指标族收敛
+ let list = ensureDemoMetricFamilyIfNeeded(
+ enrichAndCollapseMetricTypeList(
+ data.list.map(item => ({
+ ...item,
+ selection: false,
+ }))
+ ) as IMetricItem[]
+ );
+ if (this.selectedMetricType !== METRIC_TYPE_ALL) {
+ list = list.filter(item => item.metric_type === this.selectedMetricType);
+ }
+ this.metricTable = list;
+ this.tableInstance.total = this.selectedMetricType !== METRIC_TYPE_ALL ? list.length : data.total;
this.$nextTick(() => {
this.updateAllSelection();
});
@@ -417,6 +466,56 @@ export default class MetricList extends tsc {
return dayjs.tz(timestamp).format('YYYY-MM-DD HH:mm:ss');
}
+ /**
+ * 展开/收起指标族真实成员(不生成不存在的成员)
+ */
+ toggleFamilyExpand(row: IMetricItem, index: number) {
+ const expanded = !row.__expanded;
+ this.$set(row, '__expanded', expanded);
+ if (expanded) {
+ const memberRows = row.__family_member_rows || [];
+ const children = memberRows.map(item => ({
+ ...item,
+ selection: false,
+ __is_family_child: true,
+ is_family_parent: false,
+ expandable: false,
+ // 共享成员列表,便于分组联动更新
+ __family_member_rows: memberRows,
+ }));
+ this.metricTable.splice(index + 1, 0, ...children);
+ } else {
+ let removeCount = 0;
+ for (let i = index + 1; i < this.metricTable.length; i++) {
+ if (this.metricTable[i].__is_family_child) removeCount += 1;
+ else break;
+ }
+ if (removeCount) this.metricTable.splice(index + 1, removeCount);
+ }
+ }
+
+ /**
+ * 存量人工确认与自动识别冲突时的专门处理入口
+ */
+ handleTypeConflict(row: IMetricItem) {
+ this.$bkInfo({
+ title: this.$t('类型冲突处理'),
+ subTitle: this.$t('指标「{0}」的人工确认类型与自动识别结果冲突,请确认以哪一侧为准。', [row.name]),
+ okText: this.$t('保留人工确认'),
+ cancelText: this.$t('采用上报识别'),
+ confirmFn: () => {
+ this.$set(row, 'type_conflict', false);
+ this.$set(row, 'type_source', 'manual');
+ this.$bkMessage({ theme: 'success', message: this.$t('已保留人工确认类型') });
+ },
+ cancelFn: () => {
+ this.$set(row, 'type_conflict', false);
+ this.$set(row, 'type_source', 'reported');
+ this.$bkMessage({ theme: 'success', message: this.$t('已采用上报识别类型') });
+ },
+ });
+ }
+
/**
* 显示指标详情侧边栏
* @param props 表格行属性对象,包含$index等
@@ -430,15 +529,12 @@ export default class MetricList extends tsc {
}
/**
- * 处理分组选择切换
+ * 处理分组选择切换(指标族与子指标分组必须一致,联动保存)
* @param id 选中的分组ID
* @param row 当前指标行数据
*/
handleGroupSelectToggle(id: number, row: IMetricItem) {
const name = this.groupSelectList.find(item => item.id === id)?.name;
- if (name === row.scope_name) {
- return;
- }
let infoObj = {
id,
name,
@@ -447,9 +543,77 @@ export default class MetricList extends tsc {
// 未分组
infoObj = this.defaultGroupInfo;
}
+ if (infoObj.name === row.scope_name && infoObj.id === row.scope_id) {
+ return;
+ }
+ if (row.is_family_parent || row.__is_family_child) {
+ this.updateFamilyScope(infoObj, row);
+ return;
+ }
this.updateCustomFields('scope', infoObj, row, true);
}
+ /**
+ * 获取指标族联动更新目标(真实子指标列表)
+ */
+ getFamilyScopeTargets(metricInfo: IMetricItem): IMetricItem[] {
+ const members = metricInfo.__family_member_rows;
+ if ((metricInfo.is_family_parent || metricInfo.__is_family_child) && members?.length) {
+ return members;
+ }
+ return [metricInfo];
+ }
+
+ /**
+ * 指标族分组联动更新:父级与子指标必须同组,批量保存后刷新
+ */
+ async updateFamilyScope(scopeInfo: { id: number; name: string }, metricInfo: IMetricItem) {
+ const targets = this.getFamilyScopeTargets(metricInfo);
+ try {
+ const update_fields = targets.map(item => ({
+ type: 'metric',
+ name: item.name,
+ id: item.id,
+ scope: scopeInfo,
+ config: item.config,
+ }));
+ const params = {
+ time_series_group_id: this.timeSeriesGroupId,
+ update_fields,
+ };
+ if (this.isAPM) {
+ delete params.time_series_group_id;
+ Object.assign(params, {
+ app_name: this.appName,
+ service_name: this.serviceName,
+ });
+ }
+ await this.requestHandlerMap.modifyCustomTsFields(params);
+ for (const item of targets) {
+ item.scope_id = scopeInfo.id;
+ item.scope_name = scopeInfo.name;
+ }
+ // 同步表格中已展开的父/子行展示
+ for (const row of this.metricTable) {
+ if (row === metricInfo || row.__family_member_rows === metricInfo.__family_member_rows) {
+ row.scope_id = scopeInfo.id;
+ row.scope_name = scopeInfo.name;
+ }
+ if (row.__is_family_child && targets.some(item => item.id === row.id)) {
+ row.scope_id = scopeInfo.id;
+ row.scope_name = scopeInfo.name;
+ }
+ }
+ this.$bkMessage({
+ theme: 'success',
+ message: this.$t('指标组和其下方的子指标分组必须一致,均保存成功'),
+ });
+ this.$emit('refresh');
+ } catch (error) {
+ console.error('Update family scope failed:', error);
+ }
+ }
+
/**
* 判断指标是否由匹配规则生成,如果是则禁用选择
* @param metricName 指标名称
@@ -534,47 +698,96 @@ export default class MetricList extends tsc {
/** 获取表格组件,包含所有列的定义和插槽 */
getTableComponent() {
- /* 名称 */
+ /* 指标/指标族 */
const nameSlot = {
default: props => (
{
e.stopPropagation();
+ // 聚合父指标仅支持查看;真实子指标和普通指标可编辑详情
this.showMetricDetail(props);
}}
>
- {props.row.name || '--'}
+ {props.row.expandable ? (
+ {
+ e.stopPropagation();
+ this.toggleFamilyExpand(props.row, props.$index);
+ }}
+ />
+ ) : (
+
+ )}
+
+ {props.row.__is_family_child
+ ? parseMetricFamilyName(props.row.name)?.suffix || props.row.name
+ : props.row.name || '--'}
+
),
};
- /* 别名 */
- const aliasSlot = {
- default: props => (
- this.handleDescFocus(props)}
- >
-
{
- this.editingIndex = -1;
- this.handleEditDescription(props.row);
- }}
- onChange={v => {
- this.copyAlias = v;
- }}
- onEnter={() => {
- this.handleEditDescription(props.row);
- }}
- />
+ const typeSlot = {
+ default: ({ row }) => ,
+ };
+ const typeSourceSlot = {
+ default: ({ row }) => (
+
+ {getMetricTypeSourceLabel(row.type_source)}
+ {row.type_conflict && (
+ {
+ e.stopPropagation();
+ this.handleTypeConflict(row);
+ }}
+ >
+ {this.$t('处理')}
+
+ )}
),
};
+ /* 别名 */
+ const aliasSlot = {
+ default: props =>
+ props.row.is_family_parent ? (
+ --
+ ) : (
+ this.handleDescFocus(props)}
+ >
+ {
+ this.editingIndex = -1;
+ this.handleEditDescription(props.row);
+ }}
+ onChange={v => {
+ this.copyAlias = v;
+ }}
+ onEnter={() => {
+ this.handleEditDescription(props.row);
+ }}
+ />
+
+ ),
+ };
/* 分组 */
const groupSlot = {
default: ({ row, $index }) => (
@@ -600,7 +813,7 @@ export default class MetricList extends tsc {
),
};
- const { name, group, alias, hidden } = this.fieldSettingData;
+ const { name, group, alias, hidden, metricType, typeSource } = this.fieldSettingData;
return (
{
default: ({ row }) => (
this.handleRowCheck(row)}
/>
),
@@ -647,12 +865,30 @@ export default class MetricList extends tsc {
)}
+ {metricType?.checked && (
+
+ )}
+ {typeSource?.checked && (
+
+ )}
{alias.checked && (
{
handleCheckChange({ value }) {
const isCheckAll = value === 2;
for (const item of this.metricTable) {
- if (!item.movable) {
+ if (!item.movable || item.is_family_parent) {
continue;
}
if (isCheckAll) {
@@ -790,7 +1026,7 @@ export default class MetricList extends tsc {
*/
updateAllSelection(): void {
for (const item of this.metricTable) {
- if (!item.movable) {
+ if (!item.movable || item.is_family_parent) {
continue;
}
item.selection = !!this.selectedMetricMap[item.id];
@@ -965,7 +1201,7 @@ export default class MetricList extends tsc {
{
groupFilterList: string[] = [];
/** 当前选中的分组信息 */
selectedGroupInfo = { id: 0, name: '' };
+ /** 当前选中的指标类型(与分组 AND) */
+ selectedMetricType: MetricTypeValue | typeof METRIC_TYPE_ALL = METRIC_TYPE_ALL;
/** 分组管理列表 */
groupList: IGroupListItem[] = [];
/** 每个指标包含的组映射,key 为指标名称,value 为分组信息 */
@@ -371,6 +374,13 @@ export default class IndicatorDimension extends tsc {
this.groupFilterList = groupInfo.name === ALL_LABEL ? [] : [groupInfo.name];
}
+ /**
+ * 更改指标类型过滤(与自定义分组 AND)
+ */
+ changeMetricTypeFilter(metricType: MetricTypeValue | typeof METRIC_TYPE_ALL): void {
+ this.selectedMetricType = metricType;
+ }
+
/**
* 提交分组信息
* @param config 分组配置
@@ -579,6 +589,7 @@ export default class IndicatorDimension extends tsc {
groupList={this.groupList}
isSearchMode={false}
onChangeGroup={this.changeGroupFilterList}
+ onChangeMetricType={this.changeMetricTypeFilter}
onEditGroupSuccess={this.handleEditGroupSuccess}
onGroupDelByName={this.handleDelGroup}
/>
@@ -631,6 +642,7 @@ export default class IndicatorDimension extends tsc {
metricGroupsMap={this.metricGroupsMap}
timeSeriesGroupId={this.timeSeriesGroupId}
selectedGroupInfo={this.selectedGroupInfo}
+ selectedMetricType={this.selectedMetricType}
unitList={this.unitList}
onAliasChange={this.handleAliasChange}
onHandleBatchAddGroup={this.handleBatchAddGroup}
diff --git a/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/components/metric-type-tag/index.scss b/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/components/metric-type-tag/index.scss
new file mode 100644
index 00000000000..1d2d3e815a2
--- /dev/null
+++ b/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/components/metric-type-tag/index.scss
@@ -0,0 +1,34 @@
+.metric-type-tag {
+ white-space: nowrap;
+ border: none;
+
+ &.is-small {
+ height: 20px;
+ line-height: 18px;
+ }
+
+ &.is-gauge {
+ color: #1768e5;
+ background: #e1ecff;
+ }
+
+ &.is-counter {
+ color: #07899b;
+ background: #e3f8fa;
+ }
+
+ &.is-histogram {
+ color: #7b4dd8;
+ background: #f0e7ff;
+ }
+
+ &.is-summary {
+ color: #169b85;
+ background: #e5f6ee;
+ }
+
+ &.is-unclassified {
+ color: #63656e;
+ background: #f0f1f5;
+ }
+}
diff --git a/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/components/metric-type-tag/index.tsx b/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/components/metric-type-tag/index.tsx
new file mode 100644
index 00000000000..7a4745b2141
--- /dev/null
+++ b/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/components/metric-type-tag/index.tsx
@@ -0,0 +1,49 @@
+/*
+ * Tencent is pleased to support the open source community by making
+ * 蓝鲸智云PaaS平台 (BlueKing PaaS) available.
+ *
+ * Copyright (C) 2017-2025 Tencent. All rights reserved.
+ *
+ * 蓝鲸智云PaaS平台 (BlueKing PaaS) is licensed under the MIT License.
+ *
+ * License for 蓝鲸智云PaaS平台 (BlueKing PaaS):
+ *
+ * ---------------------------------------------------
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+ * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of
+ * the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
+ * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+import { Component, Prop } from 'vue-property-decorator';
+import { Component as tsc } from 'vue-tsx-support';
+
+import { type MetricTypeValue, getMetricTypeLabel } from '../../metric-type';
+
+import './index.scss';
+
+interface IProps {
+ type?: MetricTypeValue;
+ size?: 'small' | 'normal';
+}
+
+@Component
+export default class MetricTypeTag extends tsc {
+ @Prop({ type: String, default: 'unclassified' }) type: MetricTypeValue;
+ @Prop({ type: String, default: 'normal' }) size: IProps['size'];
+
+ render() {
+ const metricType = this.type || 'unclassified';
+ return (
+ {getMetricTypeLabel(metricType)}
+ );
+ }
+}
diff --git a/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/index.tsx b/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/index.tsx
index 73100b771a6..dca3550c8d4 100644
--- a/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/index.tsx
+++ b/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/index.tsx
@@ -52,9 +52,9 @@ export type ICustomTsFields = ServiceReturnType;
export default class CustomEscalationDetailNew extends tsc {
loading = false; // 加载状态
copyIsPlatform = false; // 是否为平台指标、事件
- helpPanelStorageKey = '__custom_escalation_help_panel__';
- // 是否显示右侧帮助栏(默认显示,除非明确设置为 'false')
- isShowHelpPanel = (localStorage.getItem(this.helpPanelStorageKey) ?? 'true') === 'true';
+ helpPanelStorageKey = '__custom_escalation_help_panel_v2__';
+ // 是否显示右侧帮助栏(默认收起)
+ isShowHelpPanel = (localStorage.getItem(this.helpPanelStorageKey) ?? 'false') === 'true';
// 详情数据
detailData: ICustomTimeSeriesDetail = {
diff --git a/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/metric-type.ts b/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/metric-type.ts
new file mode 100644
index 00000000000..59b5c6cc948
--- /dev/null
+++ b/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/metric-manage/metric-type.ts
@@ -0,0 +1,450 @@
+/*
+ * Tencent is pleased to support the open source community by making
+ * 蓝鲸智云PaaS平台 (BlueKing PaaS) available.
+ *
+ * Copyright (C) 2017-2025 Tencent. All rights reserved.
+ *
+ * 蓝鲸智云PaaS平台 (BlueKing PaaS) is licensed under the MIT License.
+ *
+ * License for 蓝鲸智云PaaS平台 (BlueKing PaaS):
+ *
+ * ---------------------------------------------------
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+ * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of
+ * the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
+ * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/** Prometheus / 自定义指标类型 */
+export type MetricTypeValue = 'unclassified' | 'gauge' | 'counter' | 'histogram' | 'summary';
+
+/** 类型来源 */
+export type MetricTypeSource = 'reported' | 'manual' | 'unrecognized';
+
+/** 时序性 */
+export type MetricTemporality = 'cumulative' | 'delta';
+
+/** 指标族成员 */
+export interface IMetricFamilyMember {
+ field_id: number;
+ name: string;
+ suffix: string;
+}
+
+/** 指标类型相关扩展字段(后端就绪后由接口返回;前端缺失时用 Mock 补齐) */
+export interface IMetricTypeMeta {
+ /** 是否为聚合父指标(指标族) */
+ is_family_parent?: boolean;
+ /** 指标族名称(父指标名) */
+ metric_family?: string;
+ /** 真实成员(仅父指标) */
+ family_members?: IMetricFamilyMember[];
+ /** 指标类型 */
+ metric_type?: MetricTypeValue;
+ /** 类型来源 */
+ type_source?: MetricTypeSource;
+ /** 时序性,默认 cumulative */
+ temporality?: MetricTemporality;
+ /** 人工确认类型与自动识别冲突 */
+ type_conflict?: boolean;
+ /** 行是否可展开(有真实成员) */
+ expandable?: boolean;
+ /** 是否为 Mock 补齐数据 */
+ __mock_type_meta?: boolean;
+}
+
+export const METRIC_TYPE_ALL = '__all_metric_type__';
+
+export const METRIC_TYPE_OPTIONS: { id: MetricTypeValue | typeof METRIC_TYPE_ALL; name: string }[] = [
+ { id: METRIC_TYPE_ALL, name: window.i18n.tc('全部') },
+ { id: 'unclassified', name: window.i18n.tc('待分类') },
+ { id: 'gauge', name: 'Gauge' },
+ { id: 'counter', name: 'Counter' },
+ { id: 'histogram', name: 'Histogram' },
+ { id: 'summary', name: 'Summary' },
+];
+
+export const METRIC_TYPE_LABEL_MAP: Record = {
+ unclassified: window.i18n.tc('待分类'),
+ gauge: 'Gauge',
+ counter: 'Counter',
+ histogram: 'Histogram',
+ summary: 'Summary',
+};
+
+export const METRIC_TYPE_SOURCE_LABEL_MAP: Record = {
+ reported: window.i18n.tc('上报识别'),
+ manual: window.i18n.tc('人工确认'),
+ unrecognized: window.i18n.tc('未识别'),
+};
+
+export const METRIC_TEMPORALITY_OPTIONS: { id: MetricTemporality; name: string }[] = [
+ { id: 'cumulative', name: window.i18n.tc('累计') },
+ { id: 'delta', name: window.i18n.tc('增量') },
+];
+
+/** Histogram / Summary 常见成员后缀 */
+const FAMILY_SUFFIXES = ['_bucket', '_sum', '_count', '_created'] as const;
+
+const MOCK_TYPE_CYCLE: MetricTypeValue[] = ['gauge', 'counter', 'histogram', 'summary', 'unclassified'];
+
+/**
+ * 从指标名解析族名与后缀
+ */
+export function parseMetricFamilyName(name: string): { family: string; suffix: string } | null {
+ for (const suffix of FAMILY_SUFFIXES) {
+ if (name.endsWith(suffix) && name.length > suffix.length) {
+ return { family: name.slice(0, -suffix.length), suffix };
+ }
+ }
+ return null;
+}
+
+/**
+ * 判断后端是否已返回类型元数据
+ */
+export function hasBackendMetricTypeMeta(item: IMetricTypeMeta): boolean {
+ return Boolean(item?.metric_type) && !item.__mock_type_meta;
+}
+
+/**
+ * 按名称启发式推断类型(Mock)
+ */
+function inferMockMetricType(name: string, index: number): {
+ metric_type: MetricTypeValue;
+ type_source: MetricTypeSource;
+ temporality: MetricTemporality;
+ type_conflict: boolean;
+} {
+ const lower = name.toLowerCase();
+ if (parseMetricFamilyName(name)) {
+ return {
+ metric_type: lower.includes('quantile') || lower.includes('summary') ? 'summary' : 'histogram',
+ type_source: 'reported',
+ temporality: 'cumulative',
+ type_conflict: false,
+ };
+ }
+ if (/(_total|_count)$/i.test(name) || /counter/i.test(name)) {
+ return {
+ metric_type: 'counter',
+ type_source: 'reported',
+ temporality: 'cumulative',
+ type_conflict: false,
+ };
+ }
+ if (/(_gauge|usage|ratio|percent)$/i.test(name) || /gauge/i.test(name)) {
+ return {
+ metric_type: 'gauge',
+ type_source: 'reported',
+ temporality: 'delta',
+ type_conflict: false,
+ };
+ }
+ if (/pending|unknown|todo/i.test(name)) {
+ return {
+ metric_type: 'unclassified',
+ type_source: 'unrecognized',
+ temporality: 'cumulative',
+ type_conflict: false,
+ };
+ }
+ const metric_type = MOCK_TYPE_CYCLE[index % MOCK_TYPE_CYCLE.length];
+ const type_source: MetricTypeSource =
+ metric_type === 'unclassified' ? 'unrecognized' : index % 5 === 0 ? 'manual' : 'reported';
+ return {
+ metric_type,
+ type_source,
+ temporality: metric_type === 'counter' ? 'cumulative' : index % 3 === 0 ? 'delta' : 'cumulative',
+ type_conflict: type_source === 'manual' && index % 7 === 0,
+ };
+}
+
+type MetricListItem = {
+ id: number;
+ name: string;
+ config: Record;
+ [key: string]: any;
+} & IMetricTypeMeta;
+
+/**
+ * 为指标列表补齐类型元数据,并将 Histogram/Summary 成员收敛为可展开的聚合父指标
+ * - 后端已有 metric_type 时优先使用,仅补齐缺失字段
+ * - 否则按名称 Mock,并按后缀聚合指标族
+ */
+export function enrichAndCollapseMetricTypeList(list: T[]): T[] {
+ if (!list?.length) return [];
+
+ const backendReady = list.some(item => hasBackendMetricTypeMeta(item));
+ const enriched = list.map((item, index) => {
+ if (item.metric_type) {
+ return {
+ ...item,
+ type_source: item.type_source || 'reported',
+ temporality: item.temporality || 'cumulative',
+ type_conflict: Boolean(item.type_conflict),
+ is_family_parent: Boolean(item.is_family_parent),
+ expandable: Boolean(item.expandable || item.family_members?.length),
+ __mock_type_meta: Boolean(item.__mock_type_meta),
+ } as T;
+ }
+ const inferred = inferMockMetricType(item.name, index);
+ const parsed = parseMetricFamilyName(item.name);
+ return {
+ ...item,
+ ...inferred,
+ metric_family: parsed?.family,
+ is_family_parent: false,
+ expandable: false,
+ family_members: [],
+ __mock_type_meta: true,
+ } as T;
+ });
+
+ if (backendReady) {
+ // 后端已返回类型时,仍把带 family_members 的父指标标记为可展开
+ return enriched.map(item => ({
+ ...item,
+ expandable: Boolean(item.is_family_parent && item.family_members?.length),
+ }));
+ }
+
+ // Mock:按族名聚合 _bucket/_sum/_count 等真实成员
+ const familyMap = new Map();
+ const normals: T[] = [];
+
+ for (const item of enriched) {
+ const parsed = parseMetricFamilyName(item.name);
+ if (!parsed) {
+ normals.push(item);
+ continue;
+ }
+ const bucket = familyMap.get(parsed.family) || [];
+ bucket.push(item);
+ familyMap.set(parsed.family, bucket);
+ }
+
+ const parents: T[] = [];
+ for (const [family, members] of familyMap.entries()) {
+ if (members.length < 2) {
+ // 只有单个后缀成员时不造父指标,避免凭空生成不存在的成员
+ normals.push(...members);
+ continue;
+ }
+ const base = members[0];
+ const metric_type: MetricTypeValue =
+ members.some(m => m.name.endsWith('_bucket')) || base.metric_type === 'histogram' ? 'histogram' : 'summary';
+ parents.push({
+ ...base,
+ id: base.id,
+ name: family,
+ metric_family: family,
+ metric_type,
+ type_source: 'reported',
+ temporality: 'cumulative',
+ type_conflict: false,
+ is_family_parent: true,
+ expandable: true,
+ movable: false,
+ family_members: members.map(m => {
+ const parsed = parseMetricFamilyName(m.name);
+ return {
+ field_id: m.id,
+ name: m.name,
+ suffix: parsed?.suffix || '',
+ };
+ }),
+ __mock_type_meta: true,
+ // 保留原始成员,展开行渲染用
+ __family_member_rows: members,
+ } as T);
+ }
+
+ return [...parents, ...normals];
+}
+
+/**
+ * 若列表中完全没有可收敛的指标族,注入一组演示用 Histogram 族数据,便于前端联调
+ */
+export function ensureDemoMetricFamilyIfNeeded(list: T[]): T[] {
+ if (!list?.length) return list;
+ if (list.some(item => item.is_family_parent)) return list;
+
+ const template = list[0];
+ const demoIdBase = 900000 + (template.id % 1000);
+ const family = 'http_request_duration_seconds';
+ const members = ['_bucket', '_sum', '_count'].map((suffix, idx) => ({
+ ...template,
+ id: demoIdBase + idx + 1,
+ name: `${family}${suffix}`,
+ movable: true,
+ metric_type: 'histogram' as MetricTypeValue,
+ type_source: 'reported' as MetricTypeSource,
+ temporality: 'cumulative' as MetricTemporality,
+ type_conflict: false,
+ is_family_parent: false,
+ expandable: false,
+ family_members: [],
+ __mock_type_meta: true,
+ selection: false,
+ }));
+
+ const parent = {
+ ...template,
+ id: demoIdBase,
+ name: family,
+ movable: false,
+ metric_type: 'histogram' as MetricTypeValue,
+ type_source: 'reported' as MetricTypeSource,
+ temporality: 'cumulative' as MetricTemporality,
+ type_conflict: false,
+ is_family_parent: true,
+ expandable: true,
+ metric_family: family,
+ family_members: members.map(m => ({
+ field_id: m.id,
+ name: m.name,
+ suffix: m.name.slice(family.length),
+ })),
+ __family_member_rows: members,
+ __mock_type_meta: true,
+ selection: false,
+ } as T;
+
+ // 再造一条冲突样例,便于验证「处理」入口
+ const conflictItem = {
+ ...template,
+ id: demoIdBase + 10,
+ name: 'demo_manual_conflict_metric',
+ metric_type: 'gauge' as MetricTypeValue,
+ type_source: 'manual' as MetricTypeSource,
+ temporality: 'cumulative' as MetricTemporality,
+ type_conflict: true,
+ is_family_parent: false,
+ expandable: false,
+ __mock_type_meta: true,
+ selection: false,
+ } as T;
+
+ return [parent, conflictItem, ...list];
+}
+
+/**
+ * 可视化指标树:将带后缀的成员收敛为聚合指标名,不展示子指标
+ */
+export function collapseMetricTreeMetrics<
+ T extends { alias: string; field_id: number; metric_name: string; metric_type?: MetricTypeValue; is_family_parent?: boolean; family_members?: IMetricFamilyMember[] },
+>(metrics: T[]): T[] {
+ if (!metrics?.length) return [];
+ const familyMap = new Map();
+ const normals: T[] = [];
+
+ for (const item of metrics) {
+ if (item.is_family_parent || item.metric_type === 'histogram' || item.metric_type === 'summary') {
+ if (!parseMetricFamilyName(item.metric_name)) {
+ normals.push({ ...item, is_family_parent: true, metric_type: item.metric_type || 'histogram' });
+ continue;
+ }
+ }
+ const parsed = parseMetricFamilyName(item.metric_name);
+ if (!parsed) {
+ const index = normals.length;
+ const inferred = item.metric_type ? null : inferMockMetricType(item.metric_name, index);
+ normals.push({
+ ...item,
+ metric_type: item.metric_type || inferred?.metric_type,
+ });
+ continue;
+ }
+ const bucket = familyMap.get(parsed.family) || [];
+ bucket.push(item);
+ familyMap.set(parsed.family, bucket);
+ }
+
+ const parents: T[] = [];
+ for (const [family, members] of familyMap.entries()) {
+ if (members.length < 2) {
+ normals.push(...members.map(m => ({ ...m, metric_type: m.metric_type || 'histogram' })));
+ continue;
+ }
+ const base = members[0];
+ parents.push({
+ ...base,
+ metric_name: family,
+ alias: base.alias || family,
+ metric_type: 'histogram',
+ is_family_parent: true,
+ family_members: members.map(m => {
+ const parsed = parseMetricFamilyName(m.metric_name);
+ return { field_id: m.field_id, name: m.metric_name, suffix: parsed?.suffix || '' };
+ }),
+ });
+ }
+
+ return [...parents, ...normals];
+}
+
+/**
+ * 可视化树:无 Histogram 族时注入演示聚合指标
+ */
+export function ensureDemoMetricTreeIfNeeded<
+ T extends {
+ alias: string;
+ field_id: number;
+ metric_name: string;
+ metric_type?: MetricTypeValue;
+ is_family_parent?: boolean;
+ family_members?: IMetricFamilyMember[];
+ },
+>(metrics: T[]): T[] {
+ const collapsed = collapseMetricTreeMetrics(metrics);
+ if (collapsed.some(item => item.is_family_parent || item.metric_type === 'histogram')) {
+ return collapsed;
+ }
+ if (!collapsed.length && !metrics.length) {
+ return [
+ {
+ alias: 'http_request_duration_seconds',
+ field_id: 900001,
+ metric_name: 'http_request_duration_seconds',
+ metric_type: 'histogram',
+ is_family_parent: true,
+ family_members: [
+ { field_id: 900002, name: 'http_request_duration_seconds_bucket', suffix: '_bucket' },
+ { field_id: 900003, name: 'http_request_duration_seconds_sum', suffix: '_sum' },
+ { field_id: 900004, name: 'http_request_duration_seconds_count', suffix: '_count' },
+ ],
+ } as T,
+ ];
+ }
+ if (!collapsed.length) return collapsed;
+ const base = collapsed[0];
+ return [
+ {
+ ...base,
+ alias: 'http_request_duration_seconds',
+ field_id: 900001,
+ metric_name: 'http_request_duration_seconds',
+ metric_type: 'histogram',
+ is_family_parent: true,
+ },
+ ...collapsed,
+ ];
+}
+
+export function getMetricTypeLabel(type?: MetricTypeValue) {
+ return (type && METRIC_TYPE_LABEL_MAP[type]) || METRIC_TYPE_LABEL_MAP.unclassified;
+}
+
+export function getMetricTypeSourceLabel(source?: MetricTypeSource) {
+ return (source && METRIC_TYPE_SOURCE_LABEL_MAP[source]) || METRIC_TYPE_SOURCE_LABEL_MAP.unrecognized;
+}
diff --git a/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/service/custom_report.ts b/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/service/custom_report.ts
index a30b82fbc56..6e07984d1a4 100644
--- a/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/service/custom_report.ts
+++ b/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/service/custom_report.ts
@@ -118,6 +118,20 @@ export interface ICustomTsFields {
movable: boolean;
type: string;
update_time: number;
+ /** Prometheus 指标类型:待分类/Gauge/Counter/Histogram/Summary */
+ metric_type?: 'unclassified' | 'gauge' | 'counter' | 'histogram' | 'summary';
+ /** 类型来源:上报识别/人工确认/未识别 */
+ type_source?: 'reported' | 'manual' | 'unrecognized';
+ /** 时序性:累计/增量,默认 cumulative */
+ temporality?: 'cumulative' | 'delta';
+ /** 是否为指标族聚合父指标 */
+ is_family_parent?: boolean;
+ /** 指标族名称 */
+ metric_family?: string;
+ /** 真实成员列表 */
+ family_members?: { field_id: number; name: string; suffix?: string }[];
+ /** 人工确认与自动识别冲突 */
+ type_conflict?: boolean;
}[];
}
diff --git a/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/service/scene_view.ts b/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/service/scene_view.ts
index 111db24257b..4feaaf97ef5 100644
--- a/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/service/scene_view.ts
+++ b/bkmonitor/webpack/src/monitor-pc/pages/custom-escalation/service/scene_view.ts
@@ -79,6 +79,11 @@ interface ICustomTsMetricGroups {
alias: string;
field_id: number;
metric_name: string;
+ /** Prometheus 指标类型(后端缺失时前端 Mock) */
+ metric_type?: 'unclassified' | 'gauge' | 'counter' | 'histogram' | 'summary';
+ /** 是否为指标族聚合父指标 */
+ is_family_parent?: boolean;
+ family_members?: { field_id: number; name: string; suffix?: string }[];
}[];
name: string;
scope_id: number;