feat: initialize TJWater WebGIS frontend
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getFeatureInsightMetrics,
|
||||
getFeaturePanelModel,
|
||||
getFeaturePanelProperties,
|
||||
getFeaturePropertyLabel
|
||||
} from "./feature-properties";
|
||||
|
||||
describe("feature panel properties", () => {
|
||||
it.each([
|
||||
["pipes", ["id", "diameter", "length", "material"]],
|
||||
["junctions", ["id", "elevation", "demand", "pressure"]]
|
||||
] as const)("shows the configured %s fields", (layer, expectedKeys) => {
|
||||
const entries = getFeaturePanelProperties(layer, {}, "feature-id");
|
||||
|
||||
expect(entries.map((entry) => entry.key)).toEqual(expectedKeys);
|
||||
expect(entries[0].value).toBe("feature-id");
|
||||
});
|
||||
|
||||
it("formats pipe diameter from GeoServer DN millimeter values", () => {
|
||||
const entries = getFeaturePanelProperties("pipes", {
|
||||
id: "P-1",
|
||||
diameter: 600,
|
||||
length: 42.5,
|
||||
material: "DI"
|
||||
});
|
||||
|
||||
expect(entries.map(({ label, value }) => [label, value])).toEqual([
|
||||
["编号", "P-1"],
|
||||
["管径", "600 mm"],
|
||||
["长度", "42.50 m"],
|
||||
["材质", "球墨铸铁"]
|
||||
]);
|
||||
});
|
||||
|
||||
it("builds insight metrics from the water-network layer configuration", () => {
|
||||
const metrics = getFeatureInsightMetrics("pipes", {
|
||||
diameter: 600,
|
||||
length: 42.5,
|
||||
material: "DI",
|
||||
status: "OPEN"
|
||||
});
|
||||
|
||||
expect(metrics).toEqual([
|
||||
{ label: "管径", value: "600 mm" },
|
||||
{ label: "长度", value: "42.50 m" },
|
||||
{ label: "材质", value: "球墨铸铁" },
|
||||
{ label: "状态", value: "开启" }
|
||||
]);
|
||||
});
|
||||
|
||||
it("localizes common enumerated property values in the panel", () => {
|
||||
expect(getFeaturePanelProperties("valves", {
|
||||
id: "V-1",
|
||||
node1: "J-1",
|
||||
node2: "J-2",
|
||||
diameter: 300,
|
||||
v_type: "PRV",
|
||||
setting: "OPEN"
|
||||
}).map(({ label, value }) => [label, value])).toEqual([
|
||||
["编号", "V-1"],
|
||||
["起点节点", "J-1"],
|
||||
["终点节点", "J-2"],
|
||||
["管径", "300 mm"],
|
||||
["阀门类型", "减压阀"],
|
||||
["阀门设定", "开启"]
|
||||
]);
|
||||
|
||||
expect(getFeaturePanelProperties("scada", {
|
||||
id: "S-1",
|
||||
type: "pressure",
|
||||
associated_element_id: "J-1",
|
||||
transmission_mode: "non_realtime",
|
||||
transmission_frequency: "15min",
|
||||
reliability: "high"
|
||||
}).map(({ label, value }) => [label, value])).toEqual([
|
||||
["编号", "S-1"],
|
||||
["类型", "压力监测"],
|
||||
["关联资产", "J-1"],
|
||||
["传输方式", "非实时"],
|
||||
["传输频率", "15min"],
|
||||
["可靠性", "高"]
|
||||
]);
|
||||
|
||||
expect(getFeaturePanelProperties("scada", {
|
||||
id: "S-2",
|
||||
type: "pipe_flow"
|
||||
}).find((entry) => entry.key === "type")?.value).toBe("流量监测");
|
||||
});
|
||||
|
||||
it("uses the map feature id when the properties omit it", () => {
|
||||
const model = getFeaturePanelModel("junctions", { demand: 3.4 }, "map-feature-id");
|
||||
|
||||
expect(model.id).toBe("map-feature-id");
|
||||
expect(model.badge).toBeUndefined();
|
||||
expect(model.attributes.map((entry) => entry.key)).toEqual(["elevation", "demand", "pressure"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["valves", ["id", "node1", "node2", "diameter", "v_type", "setting"]],
|
||||
["reservoirs", ["id", "head", "pattern"]],
|
||||
["scada", ["id", "type", "associated_element_id", "transmission_mode", "transmission_frequency", "reliability"]],
|
||||
["pumps", ["id", "node1", "node2", "status"]],
|
||||
["tanks", ["id", "elevation", "init_level", "min_level", "max_level"]]
|
||||
] as const)("localizes configured %s panel field labels", (layer, keys) => {
|
||||
const entries = getFeaturePanelProperties(layer, {}, "feature-id");
|
||||
|
||||
expect(entries.map((entry) => entry.key)).toEqual(keys);
|
||||
expect(entries).toEqual(
|
||||
entries.map((entry) => ({
|
||||
...entry,
|
||||
label: getFeaturePropertyLabel(entry.key)
|
||||
}))
|
||||
);
|
||||
expect(entries.filter((entry) => entry.key !== "id").map((entry) => entry.label))
|
||||
.not.toContainEqual(expect.stringMatching(/^[a-z][a-z0-9_]*$/));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,546 @@
|
||||
import type { WaterNetworkSourceId } from "../map/sources";
|
||||
import { formatValue } from "./format-value";
|
||||
|
||||
export type LocalizedFeatureProperty = {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type FeaturePanelBadgeTone = "active" | "inactive" | "warning" | "critical" | "info";
|
||||
|
||||
export type FeaturePanelBadge = {
|
||||
label: string;
|
||||
tone: FeaturePanelBadgeTone;
|
||||
};
|
||||
|
||||
export type FeaturePanelModel = {
|
||||
id: string;
|
||||
badge?: FeaturePanelBadge;
|
||||
attributes: LocalizedFeatureProperty[];
|
||||
};
|
||||
|
||||
export type FeatureInsightMetric = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const PROPERTY_LABELS: Record<string, string> = {
|
||||
fid: "GeoServer 要素 ID",
|
||||
id: "编号",
|
||||
scada_id: "编号 UUID",
|
||||
sensor_id: "传感器 ID",
|
||||
sensor_name: "传感器名称",
|
||||
swmm_node: "SWMM 节点",
|
||||
topology_order: "拓扑序",
|
||||
distance_to_wwtp_m: "距污水厂距离",
|
||||
upstream_node_count: "上游节点数",
|
||||
upstream_sensors: "上游传感器",
|
||||
downstream_path: "下游路径",
|
||||
x: "X 坐标",
|
||||
y: "Y 坐标",
|
||||
cod_mg_l: "COD",
|
||||
氨氮_mg_l: "氨氮",
|
||||
电导率_us_cm: "电导率",
|
||||
液位_m: "液位",
|
||||
流量_m3_s: "流量",
|
||||
gid: "要素编号",
|
||||
name: "名称",
|
||||
code: "编码",
|
||||
type: "类型",
|
||||
node1: "起点节点",
|
||||
node2: "终点节点",
|
||||
from_node: "起点节点",
|
||||
to_node: "终点节点",
|
||||
diameter: "管径",
|
||||
dn: "管径",
|
||||
length: "长度",
|
||||
roughness: "粗糙系数",
|
||||
shape: "断面形状",
|
||||
max_depth: "最大深度",
|
||||
invert_elevation: "井底高程",
|
||||
v_type: "阀门类型",
|
||||
setting: "阀门设定",
|
||||
minor_loss: "局部损失系数",
|
||||
orifice_type: "孔口类型",
|
||||
discharge_coeff: "流量系数",
|
||||
gated: "闸门",
|
||||
offset_height: "偏移高度",
|
||||
geom1: "几何参数 1",
|
||||
geom2: "几何参数 2",
|
||||
pump_curve: "泵曲线",
|
||||
startup_depth: "启动水深",
|
||||
shutoff_depth: "停泵水深",
|
||||
outfall_type: "排放类型",
|
||||
stage_data: "阶段数据",
|
||||
status: "状态",
|
||||
head: "水头",
|
||||
pattern: "模式",
|
||||
init_level: "初始水位",
|
||||
min_level: "最低水位",
|
||||
max_level: "最高水位",
|
||||
material: "材质",
|
||||
elevation: "高程",
|
||||
demand: "需水量",
|
||||
pressure: "压力",
|
||||
flow: "流量",
|
||||
velocity: "流速",
|
||||
zone: "分区",
|
||||
district: "片区",
|
||||
source: "来源",
|
||||
updated_at: "更新时间",
|
||||
point_id: "测点 ID",
|
||||
point_external_id: "测点编号",
|
||||
point_name: "测点名称",
|
||||
associated_element_id: "关联资产",
|
||||
transmission_mode: "传输方式",
|
||||
transmission_frequency: "传输频率",
|
||||
reliability: "可靠性",
|
||||
device_id: "设备 ID",
|
||||
device_external_id: "设备编号",
|
||||
device_type_id: "设备类型 ID",
|
||||
device_type_name: "设备类型",
|
||||
device_icon_code: "设备图标编码",
|
||||
active: "运行状态",
|
||||
external_id: "设备编号",
|
||||
junction_id: "关联检查井",
|
||||
kind: "设备类型",
|
||||
location_source: "位置来源",
|
||||
site_external_id: "站点编号",
|
||||
synced_at: "同步时间"
|
||||
};
|
||||
|
||||
const PROPERTY_ORDER: Record<string, number> = {
|
||||
fid: 9,
|
||||
id: 10,
|
||||
scada_id: 10,
|
||||
sensor_id: 10,
|
||||
sensor_name: 12,
|
||||
swmm_node: 13,
|
||||
topology_order: 14,
|
||||
distance_to_wwtp_m: 15,
|
||||
upstream_node_count: 16,
|
||||
upstream_sensors: 17,
|
||||
downstream_path: 18,
|
||||
cod_mg_l: 20,
|
||||
氨氮_mg_l: 21,
|
||||
电导率_us_cm: 22,
|
||||
液位_m: 23,
|
||||
流量_m3_s: 24,
|
||||
gid: 11,
|
||||
name: 12,
|
||||
code: 13,
|
||||
diameter: 20,
|
||||
dn: 20,
|
||||
length: 21,
|
||||
roughness: 22,
|
||||
max_depth: 30,
|
||||
invert_elevation: 31,
|
||||
orifice_type: 32,
|
||||
outfall_type: 32,
|
||||
shape: 33,
|
||||
pump_curve: 34,
|
||||
material: 23,
|
||||
node1: 24,
|
||||
from_node: 24,
|
||||
node2: 25,
|
||||
to_node: 25,
|
||||
elevation: 30,
|
||||
demand: 31,
|
||||
pressure: 32,
|
||||
flow: 33,
|
||||
velocity: 34,
|
||||
status: 90
|
||||
};
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
on: "运行",
|
||||
off: "停止",
|
||||
active: "运行中",
|
||||
inactive: "停用",
|
||||
online: "在线",
|
||||
offline: "离线",
|
||||
open: "开启",
|
||||
closed: "关闭",
|
||||
normal: "正常",
|
||||
warning: "告警",
|
||||
critical: "严重"
|
||||
};
|
||||
|
||||
const VALVE_TYPE_LABELS: Record<string, string> = {
|
||||
prv: "减压阀",
|
||||
psv: "持压阀",
|
||||
pbv: "压降控制阀",
|
||||
fcv: "流量控制阀",
|
||||
tcv: "节流控制阀",
|
||||
gpv: "通用阀",
|
||||
cv: "止回阀"
|
||||
};
|
||||
|
||||
const MATERIAL_LABELS: Record<string, string> = {
|
||||
di: "球墨铸铁",
|
||||
ci: "铸铁",
|
||||
steel: "钢管",
|
||||
st: "钢管",
|
||||
pvc: "PVC",
|
||||
pe: "PE",
|
||||
hdpe: "HDPE",
|
||||
concrete: "混凝土",
|
||||
rc: "钢筋混凝土"
|
||||
};
|
||||
|
||||
const ASSET_TYPE_LABELS: Record<string, string> = {
|
||||
pipe: "管线",
|
||||
conduit: "管线",
|
||||
junction: "节点",
|
||||
node: "节点",
|
||||
valve: "阀门",
|
||||
reservoir: "水库",
|
||||
tank: "水箱",
|
||||
pump: "泵站",
|
||||
scada: "SCADA",
|
||||
pressure: "压力监测",
|
||||
pipe_flow: "流量监测"
|
||||
};
|
||||
|
||||
const TRANSMISSION_MODE_LABELS: Record<string, string> = {
|
||||
wired: "有线",
|
||||
wire: "有线",
|
||||
cable: "有线",
|
||||
wireless: "无线",
|
||||
cellular: "蜂窝网络",
|
||||
mobile: "移动网络",
|
||||
gprs: "GPRS",
|
||||
"4g": "4G",
|
||||
"5g": "5G",
|
||||
nb_iot: "NB-IoT",
|
||||
nbiot: "NB-IoT",
|
||||
lora: "LoRa",
|
||||
realtime: "实时",
|
||||
real_time: "实时",
|
||||
non_realtime: "非实时",
|
||||
non_real_time: "非实时"
|
||||
};
|
||||
|
||||
const RELIABILITY_LABELS: Record<string, string> = {
|
||||
high: "高",
|
||||
medium: "中",
|
||||
mid: "中",
|
||||
low: "低",
|
||||
good: "良好",
|
||||
fair: "一般",
|
||||
poor: "较差"
|
||||
};
|
||||
|
||||
const PATTERN_LABELS: Record<string, string> = {
|
||||
none: "无",
|
||||
null: "无",
|
||||
default: "默认"
|
||||
};
|
||||
|
||||
const OUTFALL_TYPE_LABELS: Record<string, string> = {
|
||||
free: "自由出流"
|
||||
};
|
||||
|
||||
const GATED_LABELS: Record<string, string> = {
|
||||
no: "否",
|
||||
false: "否",
|
||||
"0": "否",
|
||||
yes: "是",
|
||||
true: "是",
|
||||
"1": "是"
|
||||
};
|
||||
|
||||
const SCADA_KIND_LABELS: Record<string, string> = {
|
||||
water_quality: "水质监测",
|
||||
radar_level: "雷达液位",
|
||||
ultrasonic_flow: "超声流量",
|
||||
integrated_monitoring: "综合监测点"
|
||||
};
|
||||
|
||||
export const SCADA_METRIC_PROPERTY_KEYS = [
|
||||
"COD_mg_L",
|
||||
"氨氮_mg_L",
|
||||
"电导率_uS_cm",
|
||||
"液位_m",
|
||||
"流量_m3_s"
|
||||
] as const;
|
||||
|
||||
const PROPERTY_UNITS: Record<string, string> = {
|
||||
cod_mg_l: "mg/L",
|
||||
氨氮_mg_l: "mg/L",
|
||||
电导率_us_cm: "μS/cm",
|
||||
液位_m: "m",
|
||||
流量_m3_s: "m³/s"
|
||||
};
|
||||
|
||||
const FEATURE_PANEL_PROPERTY_KEYS: Record<WaterNetworkSourceId, readonly string[]> = {
|
||||
pipes: ["id", "diameter", "length", "material"],
|
||||
junctions: ["id", "elevation", "demand", "pressure"],
|
||||
valves: ["id", "node1", "node2", "diameter", "v_type", "setting"],
|
||||
reservoirs: ["id", "head", "pattern"],
|
||||
scada: ["id", "type", "associated_element_id", "transmission_mode", "transmission_frequency", "reliability"],
|
||||
pumps: ["id", "node1", "node2", "status"],
|
||||
tanks: ["id", "elevation", "init_level", "min_level", "max_level"]
|
||||
};
|
||||
|
||||
const FEATURE_PANEL_BADGE_KEYS: Partial<Record<WaterNetworkSourceId, string>> = {
|
||||
pipes: "status",
|
||||
valves: "setting"
|
||||
};
|
||||
|
||||
type FeatureInsightMetricDefinition = {
|
||||
label: string;
|
||||
key?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
const FEATURE_INSIGHT_METRICS: Record<WaterNetworkSourceId, readonly FeatureInsightMetricDefinition[]> = {
|
||||
pipes: [
|
||||
{ label: "管径", key: "diameter" },
|
||||
{ label: "长度", key: "length" },
|
||||
{ label: "材质", key: "material" },
|
||||
{ label: "状态", key: "status" }
|
||||
],
|
||||
junctions: [
|
||||
{ label: "需水量", key: "demand" },
|
||||
{ label: "高程", key: "elevation" },
|
||||
{ label: "压力", key: "pressure" },
|
||||
{ label: "状态", value: "在线" }
|
||||
],
|
||||
valves: [
|
||||
{ label: "管径", key: "diameter" },
|
||||
{ label: "类型", key: "v_type" },
|
||||
{ label: "设定", key: "setting" },
|
||||
{ label: "局部损失", key: "minor_loss" }
|
||||
],
|
||||
reservoirs: [
|
||||
{ label: "水头", key: "head" },
|
||||
{ label: "模式", key: "pattern" },
|
||||
{ label: "状态", value: "在线" }
|
||||
],
|
||||
scada: [
|
||||
{ label: "类型", key: "type" },
|
||||
{ label: "关联资产", key: "associated_element_id" },
|
||||
{ label: "传输方式", key: "transmission_mode" },
|
||||
{ label: "可靠性", key: "reliability" }
|
||||
],
|
||||
pumps: [
|
||||
{ label: "起点", key: "node1" },
|
||||
{ label: "终点", key: "node2" },
|
||||
{ label: "状态", key: "status" }
|
||||
],
|
||||
tanks: [
|
||||
{ label: "高程", key: "elevation" },
|
||||
{ label: "初始水位", key: "init_level" },
|
||||
{ label: "最高水位", key: "max_level" },
|
||||
{ label: "状态", value: "在线" }
|
||||
]
|
||||
};
|
||||
|
||||
export function getLocalizedFeatureProperties(properties: Record<string, unknown>): LocalizedFeatureProperty[] {
|
||||
return Object.entries(properties).filter(([key]) => normalizePropertyKey(key) !== "cluster_size").map(([key, value]) => ({
|
||||
key,
|
||||
label: getFeaturePropertyLabel(key),
|
||||
value: formatFeaturePropertyValue(key, value)
|
||||
})).sort((first, second) => getPropertyOrder(first.key) - getPropertyOrder(second.key));
|
||||
}
|
||||
|
||||
export function getFeaturePanelProperties(
|
||||
layer: WaterNetworkSourceId,
|
||||
properties: Record<string, unknown>,
|
||||
featureId?: string
|
||||
): LocalizedFeatureProperty[] {
|
||||
return FEATURE_PANEL_PROPERTY_KEYS[layer].map((key) => {
|
||||
const propertyValue = getPropertyValue(properties, key);
|
||||
const value = key === "id" || key === "scada_id" || key === "sensor_id" ? propertyValue ?? featureId : propertyValue;
|
||||
|
||||
return {
|
||||
key,
|
||||
label: getFeaturePropertyLabel(key),
|
||||
value: formatFeaturePropertyValue(key, value)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function getScadaMetricProperties(
|
||||
properties: Record<string, unknown>
|
||||
): LocalizedFeatureProperty[] {
|
||||
return SCADA_METRIC_PROPERTY_KEYS.map((key) => ({
|
||||
key,
|
||||
label: getFeaturePropertyLabel(key),
|
||||
value: formatFeaturePropertyValue(key, getPropertyValue(properties, key))
|
||||
}));
|
||||
}
|
||||
|
||||
export function getFeatureInsightMetrics(
|
||||
layer: WaterNetworkSourceId,
|
||||
properties: Record<string, unknown>
|
||||
): FeatureInsightMetric[] {
|
||||
return FEATURE_INSIGHT_METRICS[layer].map((metric) => ({
|
||||
label: metric.label,
|
||||
value: metric.key
|
||||
? formatFeaturePropertyValue(metric.key, getPropertyValue(properties, metric.key))
|
||||
: metric.value ?? "暂无"
|
||||
}));
|
||||
}
|
||||
|
||||
export function getFeaturePanelModel(
|
||||
layer: WaterNetworkSourceId,
|
||||
properties: Record<string, unknown>,
|
||||
featureId?: string
|
||||
): FeaturePanelModel {
|
||||
const entries = getFeaturePanelProperties(layer, properties, featureId);
|
||||
const idKey = "id";
|
||||
const id = entries.find((entry) => entry.key === idKey)?.value ?? "暂无";
|
||||
const badgeKey = FEATURE_PANEL_BADGE_KEYS[layer];
|
||||
const badgeEntry = badgeKey ? entries.find((entry) => entry.key === badgeKey) : undefined;
|
||||
|
||||
return {
|
||||
id,
|
||||
badge: badgeEntry ? getFeaturePanelBadge(layer, badgeEntry.value) : undefined,
|
||||
attributes: entries.filter((entry) => entry.key !== idKey && entry.key !== badgeKey)
|
||||
};
|
||||
}
|
||||
|
||||
export function getFeaturePropertyLabel(key: string) {
|
||||
const normalizedKey = normalizePropertyKey(key);
|
||||
return PROPERTY_LABELS[normalizedKey] ?? key;
|
||||
}
|
||||
|
||||
export function formatFeaturePropertyValue(key: string, value: unknown) {
|
||||
const normalizedKey = normalizePropertyKey(key);
|
||||
|
||||
if (typeof value === "string") {
|
||||
const mappedValue = getMappedPropertyValue(normalizedKey, value);
|
||||
if (mappedValue) return mappedValue;
|
||||
}
|
||||
|
||||
if (normalizedKey === "outfall_type" && typeof value === "string") {
|
||||
return OUTFALL_TYPE_LABELS[value.toLowerCase()] ?? value;
|
||||
}
|
||||
|
||||
if (normalizedKey === "gated" && value !== null && value !== undefined && value !== "") {
|
||||
return GATED_LABELS[String(value).toLowerCase()] ?? formatValue(value);
|
||||
}
|
||||
|
||||
if (normalizedKey === "active" && typeof value === "boolean") {
|
||||
return value ? "在线" : "离线";
|
||||
}
|
||||
|
||||
if (normalizedKey === "location_source" && value === "density_cluster_demo") {
|
||||
return "密度聚类模拟位置";
|
||||
}
|
||||
|
||||
if (normalizedKey === "location_source" && value === "uniform_farthest_point_demo") {
|
||||
return "空间均匀采样模拟位置";
|
||||
}
|
||||
|
||||
if (normalizedKey === "kind" && typeof value === "string") {
|
||||
return SCADA_KIND_LABELS[value] ?? value;
|
||||
}
|
||||
|
||||
const unit = PROPERTY_UNITS[normalizedKey];
|
||||
if (unit && value !== null && value !== undefined && value !== "") {
|
||||
return `${formatValue(value)} ${unit}`;
|
||||
}
|
||||
|
||||
if ((normalizedKey === "diameter" || normalizedKey === "dn") && value !== null && value !== undefined && value !== "") {
|
||||
const diameterInMillimeters = typeof value === "number" ? value : Number(value);
|
||||
return Number.isFinite(diameterInMillimeters)
|
||||
? `${formatValue(diameterInMillimeters)} mm`
|
||||
: formatValue(value);
|
||||
}
|
||||
|
||||
if (["length", "elevation", "invert_elevation", "max_depth", "startup_depth", "shutoff_depth", "distance_to_wwtp_m"].includes(normalizedKey)
|
||||
&& value !== null && value !== undefined && value !== "") {
|
||||
return `${formatValue(value)} m`;
|
||||
}
|
||||
|
||||
return formatValue(value);
|
||||
}
|
||||
|
||||
function getPropertyOrder(key: string) {
|
||||
return PROPERTY_ORDER[normalizePropertyKey(key)] ?? 100;
|
||||
}
|
||||
|
||||
function normalizePropertyKey(key: string) {
|
||||
return key.trim().replaceAll("-", "_").toLowerCase();
|
||||
}
|
||||
|
||||
function getPropertyValue(properties: Record<string, unknown>, key: string) {
|
||||
if (Object.prototype.hasOwnProperty.call(properties, key)) {
|
||||
return properties[key];
|
||||
}
|
||||
|
||||
const normalizedKey = normalizePropertyKey(key);
|
||||
const matchingKey = Object.keys(properties).find((propertyKey) => normalizePropertyKey(propertyKey) === normalizedKey);
|
||||
return matchingKey ? properties[matchingKey] : undefined;
|
||||
}
|
||||
|
||||
function getMappedPropertyValue(normalizedKey: string, value: string) {
|
||||
const normalizedValue = normalizePropertyValue(value);
|
||||
|
||||
if (isStatusLikeKey(normalizedKey)) {
|
||||
return STATUS_LABELS[normalizedValue];
|
||||
}
|
||||
|
||||
if (normalizedKey === "v_type") {
|
||||
return VALVE_TYPE_LABELS[normalizedValue];
|
||||
}
|
||||
|
||||
if (normalizedKey === "material") {
|
||||
return MATERIAL_LABELS[normalizedValue];
|
||||
}
|
||||
|
||||
if (normalizedKey === "type") {
|
||||
return ASSET_TYPE_LABELS[normalizedValue];
|
||||
}
|
||||
|
||||
if (normalizedKey === "transmission_mode") {
|
||||
return TRANSMISSION_MODE_LABELS[normalizedValue];
|
||||
}
|
||||
|
||||
if (normalizedKey === "reliability") {
|
||||
return RELIABILITY_LABELS[normalizedValue];
|
||||
}
|
||||
|
||||
if (normalizedKey === "pattern") {
|
||||
return PATTERN_LABELS[normalizedValue];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizePropertyValue(value: string) {
|
||||
return value.trim().replaceAll("-", "_").replaceAll(" ", "_").toLowerCase();
|
||||
}
|
||||
|
||||
function isStatusLikeKey(normalizedKey: string) {
|
||||
return normalizedKey === "status" || normalizedKey === "setting";
|
||||
}
|
||||
|
||||
function getFeaturePanelBadge(
|
||||
layer: WaterNetworkSourceId,
|
||||
value: string
|
||||
): FeaturePanelBadge {
|
||||
const normalizedValue = value.toLowerCase();
|
||||
|
||||
if (["运行", "运行中", "在线", "开启", "正常"].includes(value)) {
|
||||
return { label: value, tone: "active" };
|
||||
}
|
||||
|
||||
if (["停止", "停用", "离线", "关闭"].includes(value)) {
|
||||
return { label: value, tone: "inactive" };
|
||||
}
|
||||
|
||||
if (normalizedValue === "warning" || value === "告警") {
|
||||
return { label: value, tone: "warning" };
|
||||
}
|
||||
|
||||
if (normalizedValue === "critical" || value === "严重") {
|
||||
return { label: value, tone: "critical" };
|
||||
}
|
||||
|
||||
return { label: value, tone: "inactive" };
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export function formatValue(value: unknown) {
|
||||
if (typeof value === "number") {
|
||||
return Number.isInteger(value) ? value.toString() : value.toFixed(2);
|
||||
}
|
||||
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "暂无";
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { getRunningWorkflowDefinition } from "@/features/workbench/data/running-workflows";
|
||||
import type { ScheduledConditionItem, ScheduledConditionStatus, WorkbenchAlert } from "@/features/workbench/types";
|
||||
|
||||
const SCHEDULED_CONDITION_ALERT_ID_PREFIX = "scheduled-condition-alert-";
|
||||
|
||||
const statusLabels: Record<ScheduledConditionStatus, string> = {
|
||||
completed: "完成",
|
||||
running: "执行中",
|
||||
warning: "关注",
|
||||
error: "异常",
|
||||
pending: "预约"
|
||||
};
|
||||
|
||||
export function createConditionConversationPrompt(condition: ScheduledConditionItem) {
|
||||
const workflow =
|
||||
condition.kind === "condition"
|
||||
? getRunningWorkflowDefinition(condition)
|
||||
: null;
|
||||
const evidence = condition.evidence?.map((item, index) => `${index + 1}. ${item}`).join("\n") || "无";
|
||||
const kpis =
|
||||
condition.kind === "condition" && condition.kpis?.length
|
||||
? condition.kpis
|
||||
.map((kpi, index) => {
|
||||
const value = `${kpi.value}${kpi.unit ?? ""}`;
|
||||
const threshold = kpi.threshold ? `,阈值:${kpi.threshold}` : "";
|
||||
return `${index + 1}. ${kpi.label}:${value},状态:${getRiskLabel(kpi.status)}${threshold}`;
|
||||
})
|
||||
.join("\n")
|
||||
: "无";
|
||||
const report =
|
||||
condition.kind === "condition" && condition.report
|
||||
? [
|
||||
condition.report.conclusion,
|
||||
...condition.report.sections.map(
|
||||
(section) => `${section.title}:${section.items.join(";")}`
|
||||
)
|
||||
].join("\n")
|
||||
: null;
|
||||
|
||||
return [
|
||||
"请继续分析这条工况任务,并基于当前上下文给出下一步调度建议。",
|
||||
"",
|
||||
`时间:${formatTime(condition.scheduledAt)}`,
|
||||
"类型:历史工况",
|
||||
workflow ? `任务工作流:${workflow.title}` : null,
|
||||
workflow ? `工作流步骤:${workflow.steps.map((step, index) => `${index + 1}. ${step.title}`).join(";")}` : null,
|
||||
`标题:${condition.title}`,
|
||||
`状态:${statusLabels[condition.status]}`,
|
||||
`风险:${getRiskLabel(condition.riskLevel)}`,
|
||||
`摘要:${condition.summary}`,
|
||||
condition.detail ? `详情:${condition.detail}` : null,
|
||||
`关键证据:\n${evidence}`,
|
||||
`关键指标:\n${kpis}`,
|
||||
report ? `工况报告:\n${report}` : null,
|
||||
condition.recommendation ? `已有建议:${condition.recommendation}` : null,
|
||||
condition.sessionId ? `sessionId:${condition.sessionId}` : null,
|
||||
"",
|
||||
"请先说明当前判断,再列出需要人工确认的事项和建议动作。"
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function createAlertQueueConversationPrompt({
|
||||
alerts,
|
||||
conditions
|
||||
}: {
|
||||
alerts: WorkbenchAlert[];
|
||||
conditions: ScheduledConditionItem[];
|
||||
}) {
|
||||
const alertSummaries = alerts.map((alert, index) => {
|
||||
const condition = findConditionForAlert(alert, conditions);
|
||||
const baseSummary = [
|
||||
`${index + 1}. ${alert.title}`,
|
||||
"状态:待复核",
|
||||
`时间:${alert.time}`,
|
||||
`描述:${alert.description}`
|
||||
];
|
||||
|
||||
if (!condition) {
|
||||
return baseSummary.join("\n");
|
||||
}
|
||||
|
||||
return [
|
||||
...baseSummary,
|
||||
`关联工况:${condition.title}`,
|
||||
`工况状态:${statusLabels[condition.status]}`,
|
||||
`风险等级:${getRiskLabel(condition.riskLevel)}`,
|
||||
condition.detail ? `工况详情:${condition.detail}` : null,
|
||||
condition.evidence?.length ? `关键证据:\n${condition.evidence.map((item, evidenceIndex) => `${evidenceIndex + 1}. ${item}`).join("\n")}` : null,
|
||||
condition.recommendation ? `已有建议:${condition.recommendation}` : null,
|
||||
condition.kind === "condition" && condition.analysisInsight?.solutionOptions?.length
|
||||
? `已有处置方案:\n${condition.analysisInsight.solutionOptions.map((option, optionIndex) => `${optionIndex + 1}. ${option.title}:${option.action};代价:${option.tradeoff}`).join("\n")}`
|
||||
: null
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
});
|
||||
|
||||
return [
|
||||
"请作为排水管网调度 Agent,汇总当前待复核工况队列。",
|
||||
"",
|
||||
"目标:",
|
||||
"1. 汇总这一组待复核工况之间的关联关系和处理顺序。",
|
||||
"2. 列出支撑判断的关键证据和不确定性。",
|
||||
"3. 给出处置方案,包含建议动作、适用条件、影响范围和需要人工确认的事项。",
|
||||
"4. 不要自动执行阀门、泵站或工单动作,先给调度员确认。",
|
||||
"",
|
||||
`待复核工况数量:${alerts.length}`,
|
||||
"",
|
||||
"待复核工况上下文:",
|
||||
alertSummaries.join("\n\n"),
|
||||
"",
|
||||
"请先给出总体判断,再按工况列出处置建议,最后列出需要补充核查的数据。"
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function findConditionForAlert(alert: WorkbenchAlert, conditions: ScheduledConditionItem[]) {
|
||||
if (!alert.id.startsWith(SCHEDULED_CONDITION_ALERT_ID_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const conditionId = alert.id.slice(SCHEDULED_CONDITION_ALERT_ID_PREFIX.length);
|
||||
return conditions.find((condition) => condition.id === conditionId) ?? null;
|
||||
}
|
||||
|
||||
function formatTime(value: string) {
|
||||
const match = value.match(/T(\d{2}:\d{2})/);
|
||||
return match?.[1] ?? value;
|
||||
}
|
||||
|
||||
function getRiskLabel(riskLevel: ScheduledConditionItem["riskLevel"]) {
|
||||
if (riskLevel === "critical") {
|
||||
return "需重点复核";
|
||||
}
|
||||
|
||||
if (riskLevel === "attention") {
|
||||
return "关注";
|
||||
}
|
||||
|
||||
return "正常";
|
||||
}
|
||||
Reference in New Issue
Block a user