[migrate]:[FL-27][补齐杆塔专业参数编辑与导出字段]

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
chengkai3
2026-06-07 21:42:05 +08:00
parent aebf152cd4
commit cd0d605c5b
4 changed files with 568 additions and 43 deletions
+189 -8
View File
@@ -578,24 +578,79 @@ def export_line_towers_to_csv(db: Session, *, line: Line) -> tuple[str, bytes]:
.order_by(LineTower.seq_no.asc(), LineTower.id.asc())
).scalars().all()
tower_ids = [tower.id for tower in towers]
profile_map = {
profile.tower_id: profile
for profile in (
db.execute(select(TowerProfile).where(TowerProfile.tower_id.in_(tower_ids))).scalars().all()
if tower_ids
else []
)
}
headers = [
"线路编号",
"序号",
"线路名称",
"电压等级",
"序号",
"塔号",
"杆塔模型",
"直线或耐张杆",
"",
"经度",
"纬度",
"海拔m",
"I回相序",
"II回相序",
"III回相序",
"IV回相序",
"A相是否安装避雷器",
"B相是否安装避雷器",
"C相是否安装避雷器",
"接地电阻",
"地闪密度",
"左避雷中距m",
"右避雷中距m",
"避雷线高度m",
"绝缘子串长度mm",
"杆塔呼高m",
"I回上相中距m",
"I回中相中距m",
"I回下相中距m",
"I回上相高度m",
"I回中相高度m",
"I回下相高度m",
"II回上相中距m",
"II回中相中距m",
"II回下相中距m",
"II回上相高度m",
"II回中相高度m",
"II回下相高度m",
"III回上相中距m",
"III回中相中距m",
"III回下相中距m",
"III回上相高度m",
"III回中相高度m",
"III回下相高度m",
"IV回上相中距m",
"IV回中相中距m",
"IV回下相中距m",
"IV回上相高度m",
"IV回中相高度m",
"IV回下相高度m",
"小号侧档距",
"大号侧档距",
"电角度",
"雷电流幅值a",
"雷电流幅值b",
"地面倾角1",
"地面倾角2",
"海拔m",
"地形",
"地闪密度",
"直线或耐张杆塔",
"绕击反击",
"反击耐雷水平kA",
"反击跳闸率(次/100km.a)",
"绕击耐雷水平kA",
"绕击跳闸率(次/100km.a)",
"雷击风险等级",
"几何参数JSON",
"雷电参数JSON",
@@ -607,26 +662,117 @@ def export_line_towers_to_csv(db: Session, *, line: Line) -> tuple[str, bytes]:
writer.writerow(headers)
for tower in towers:
profile = profile_map.get(tower.id)
writer.writerow(
[
line.code,
tower.seq_no,
line.name,
line.voltage_kv or "",
tower.seq_no,
tower.tower_no,
tower.tower_model or "",
tower.tower_type or "",
_to_csv_number(tower.longitude),
_to_csv_number(tower.latitude),
_to_csv_number(tower.altitude_m),
_normalize_str(_pick_export_value(profile.phase_sequence_1 if profile else None, _safe_dict(line.phase_sequence_json).get("I"))) or "",
_normalize_str(_pick_export_value(profile.phase_sequence_2 if profile else None, _safe_dict(line.phase_sequence_json).get("II"))) or "",
_normalize_str(_pick_export_value(profile.phase_sequence_3 if profile else None, _safe_dict(line.phase_sequence_json).get("III"))) or "",
_normalize_str(_pick_export_value(profile.phase_sequence_4 if profile else None, _safe_dict(line.phase_sequence_json).get("IV"))) or "",
_normalize_str(_pick_export_value(profile.arrester_a if profile else None, _safe_dict(line.arrester_install_json).get("A"))) or "",
_normalize_str(_pick_export_value(profile.arrester_b if profile else None, _safe_dict(line.arrester_install_json).get("B"))) or "",
_normalize_str(_pick_export_value(profile.arrester_c if profile else None, _safe_dict(line.arrester_install_json).get("C"))) or "",
_to_csv_number(tower.ground_resistance_ohm),
_to_csv_number(tower.lightning_density),
_to_csv_number(
_pick_export_float(
profile.protection_angle_left_deg if profile else None,
_read_nested_value(profile.geometry_layers_json if profile else None, "lightning_wire", "left_mid_distance_m"),
_read_nested_value(tower.circuit_geometry_json, "lightning_wire", "left_mid_distance_m"),
)
),
_to_csv_number(
_pick_export_float(
profile.protection_angle_right_deg if profile else None,
_read_nested_value(profile.geometry_layers_json if profile else None, "lightning_wire", "right_mid_distance_m"),
_read_nested_value(tower.circuit_geometry_json, "lightning_wire", "right_mid_distance_m"),
)
),
_to_csv_number(
_pick_export_float(
profile.shield_wire_height_m if profile else None,
_read_nested_value(profile.geometry_layers_json if profile else None, "lightning_wire", "height_m"),
_read_nested_value(tower.circuit_geometry_json, "lightning_wire", "height_m"),
)
),
_to_csv_number(
_pick_export_float(
profile.insulator_length_m if profile else None,
_read_nested_value(profile.geometry_layers_json if profile else None, "insulator_length_mm"),
_read_nested_value(tower.circuit_geometry_json, "insulator_length_mm"),
)
),
_to_csv_number(
_pick_export_float(
profile.call_height_m if profile else None,
_read_nested_value(profile.geometry_layers_json if profile else None, "tower_height_m"),
_read_nested_value(tower.circuit_geometry_json, "tower_height_m"),
)
),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "I", "phase_spacing_m", "upper"), _read_nested_value(tower.circuit_geometry_json, "I", "phase_spacing_m", "upper"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "I", "phase_spacing_m", "middle"), _read_nested_value(tower.circuit_geometry_json, "I", "phase_spacing_m", "middle"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "I", "phase_spacing_m", "lower"), _read_nested_value(tower.circuit_geometry_json, "I", "phase_spacing_m", "lower"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "I", "phase_height_m", "upper"), _read_nested_value(tower.circuit_geometry_json, "I", "phase_height_m", "upper"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "I", "phase_height_m", "middle"), _read_nested_value(tower.circuit_geometry_json, "I", "phase_height_m", "middle"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "I", "phase_height_m", "lower"), _read_nested_value(tower.circuit_geometry_json, "I", "phase_height_m", "lower"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "II", "phase_spacing_m", "upper"), _read_nested_value(tower.circuit_geometry_json, "II", "phase_spacing_m", "upper"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "II", "phase_spacing_m", "middle"), _read_nested_value(tower.circuit_geometry_json, "II", "phase_spacing_m", "middle"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "II", "phase_spacing_m", "lower"), _read_nested_value(tower.circuit_geometry_json, "II", "phase_spacing_m", "lower"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "II", "phase_height_m", "upper"), _read_nested_value(tower.circuit_geometry_json, "II", "phase_height_m", "upper"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "II", "phase_height_m", "middle"), _read_nested_value(tower.circuit_geometry_json, "II", "phase_height_m", "middle"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "II", "phase_height_m", "lower"), _read_nested_value(tower.circuit_geometry_json, "II", "phase_height_m", "lower"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "III", "phase_spacing_m", "upper"), _read_nested_value(tower.circuit_geometry_json, "III", "phase_spacing_m", "upper"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "III", "phase_spacing_m", "middle"), _read_nested_value(tower.circuit_geometry_json, "III", "phase_spacing_m", "middle"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "III", "phase_spacing_m", "lower"), _read_nested_value(tower.circuit_geometry_json, "III", "phase_spacing_m", "lower"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "III", "phase_height_m", "upper"), _read_nested_value(tower.circuit_geometry_json, "III", "phase_height_m", "upper"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "III", "phase_height_m", "middle"), _read_nested_value(tower.circuit_geometry_json, "III", "phase_height_m", "middle"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "III", "phase_height_m", "lower"), _read_nested_value(tower.circuit_geometry_json, "III", "phase_height_m", "lower"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "IV", "phase_spacing_m", "upper"), _read_nested_value(tower.circuit_geometry_json, "IV", "phase_spacing_m", "upper"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "IV", "phase_spacing_m", "middle"), _read_nested_value(tower.circuit_geometry_json, "IV", "phase_spacing_m", "middle"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "IV", "phase_spacing_m", "lower"), _read_nested_value(tower.circuit_geometry_json, "IV", "phase_spacing_m", "lower"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "IV", "phase_height_m", "upper"), _read_nested_value(tower.circuit_geometry_json, "IV", "phase_height_m", "upper"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "IV", "phase_height_m", "middle"), _read_nested_value(tower.circuit_geometry_json, "IV", "phase_height_m", "middle"))),
_to_csv_number(_pick_export_float(_read_nested_value(profile.geometry_layers_json if profile else None, "IV", "phase_height_m", "lower"), _read_nested_value(tower.circuit_geometry_json, "IV", "phase_height_m", "lower"))),
_to_csv_number(tower.span_small_m),
_to_csv_number(tower.span_large_m),
_to_csv_number(
_pick_export_float(
profile.angle_deg if profile else None,
_safe_dict(line.lightning_param_json).get("电角度"),
)
),
_to_csv_number(
_pick_export_float(
profile.current_a if profile else None,
_safe_dict(line.lightning_param_json).get("雷电流幅值a"),
)
),
_to_csv_number(
_pick_export_float(
profile.current_b if profile else None,
_safe_dict(line.lightning_param_json).get("雷电流幅值b"),
)
),
_to_csv_number(tower.slope_1),
_to_csv_number(tower.slope_2),
_to_csv_number(tower.altitude_m),
tower.terrain or "",
tower.risk_level or "",
_to_csv_number(tower.lightning_density),
_normalize_str(_pick_export_value(profile.structure_kind if profile else None, tower.tower_type)) or "",
_normalize_str(_pick_export_value(profile.stroke_mode if profile else None, _safe_dict(tower.lightning_result_json).get("counterstroke_indicator"))) or "",
_to_csv_number(_pick_export_float(_safe_dict(tower.lightning_result_json).get("counterstroke_withstand_ka"))),
_to_csv_number(_pick_export_float(_safe_dict(tower.lightning_result_json).get("counterstroke_trip_rate"))),
_to_csv_number(_pick_export_float(_safe_dict(tower.lightning_result_json).get("shielding_withstand_ka"))),
_to_csv_number(_pick_export_float(_safe_dict(tower.lightning_result_json).get("shielding_trip_rate"))),
_normalize_str(_pick_export_value(tower.risk_level, _safe_dict(tower.lightning_result_json).get("risk_level"))) or "",
_json_dumps_compact(tower.circuit_geometry_json or {}),
_json_dumps_compact(tower.lightning_result_json or {}),
_json_dumps_compact(tower.raw_extra_json or {}),
@@ -871,6 +1017,41 @@ def _json_dumps_compact(payload: dict[str, Any]) -> str:
return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
def _safe_dict(value: Any) -> dict[str, Any]:
if isinstance(value, dict):
return value
return {}
def _read_nested_value(value: Any, *keys: str) -> Any:
current: Any = value
for key in keys:
if not isinstance(current, dict):
return None
current = current.get(key)
return current
def _pick_export_value(*values: Any) -> Any:
for value in values:
if value is None:
continue
if isinstance(value, str) and not value.strip():
continue
return value
return None
def _pick_export_float(*values: Any) -> float | None:
for value in values:
if isinstance(value, (int, float)):
return float(value)
parsed = _parse_float(value)
if parsed is not None:
return parsed
return None
def _to_csv_number(value: float | None) -> str:
if value is None:
return ""
+129
View File
@@ -1,5 +1,7 @@
from __future__ import annotations
import csv
import io
from types import SimpleNamespace
from sqlalchemy import create_engine
@@ -69,3 +71,130 @@ def test_generate_line_code_skips_existing_code(monkeypatch) -> None:
assert generated == f"PL-{line_service.utcnow().strftime('%Y%m%d')}-DEF456"
finally:
session.close()
def test_export_line_towers_to_csv_includes_legacy_professional_columns() -> None:
session = _build_session()
try:
line = Line(
code="PL-LEGACY-001",
name="遗留导出线路",
voltage_kv=220,
phase_sequence_json={"I": "ABC", "II": "BCA", "III": "CAB", "IV": "CBA"},
arrester_install_json={"A": "", "B": "", "C": ""},
lightning_param_json={"电角度": 12.5, "雷电流幅值a": 28.0, "雷电流幅值b": 2.2},
status="enabled",
)
session.add(line)
session.flush()
tower = LineTower(
line_id=line.id,
seq_no=1,
tower_no="N001",
tower_model="ZM-001",
tower_type="直线塔",
longitude=120.123456,
latitude=30.654321,
altitude_m=950.0,
terrain="山区",
ground_resistance_ohm=12.5,
lightning_density=3.6,
span_small_m=210.0,
span_large_m=260.0,
slope_1=1.5,
slope_2=2.5,
risk_level="",
circuit_geometry_json={
"I": {
"phase_spacing_m": {"upper": 4.4, "middle": 3.3, "lower": 2.2},
"phase_height_m": {"upper": 30.0, "middle": 28.0, "lower": 26.0},
},
"III": {
"phase_spacing_m": {"upper": 9.1, "middle": 8.2, "lower": 7.3},
"phase_height_m": {"upper": 35.0, "middle": 34.0, "lower": 33.0},
},
"lightning_wire": {
"left_mid_distance_m": 7.7,
"right_mid_distance_m": 8.8,
"height_m": 39.0,
},
"insulator_length_mm": 4000.0,
"tower_height_m": 38.0,
},
lightning_result_json={
"counterstroke_withstand_ka": 45.0,
"counterstroke_trip_rate": 0.6,
"shielding_withstand_ka": 52.0,
"shielding_trip_rate": 0.3,
"risk_level": "",
},
)
session.add(tower)
session.flush()
session.add(
TowerProfile(
tower_id=tower.id,
phase_sequence_1="ACB",
phase_sequence_2="BAC",
arrester_a="",
arrester_b="",
arrester_c="",
protection_angle_left_deg=8.8,
protection_angle_right_deg=9.9,
shield_wire_height_m=41.0,
insulator_length_m=4200.0,
call_height_m=40.5,
angle_deg=18.0,
current_a=31.0,
current_b=2.6,
structure_kind="耐张杆塔",
stroke_mode="反击",
geometry_layers_json={
"I": {
"phase_spacing_m": {"upper": 5.1, "middle": 4.2, "lower": 3.3},
"phase_height_m": {"upper": 31.0, "middle": 29.0, "lower": 27.0},
},
"II": {
"phase_spacing_m": {"upper": 6.1, "middle": 5.2, "lower": 4.3},
"phase_height_m": {"upper": 32.0, "middle": 30.0, "lower": 28.0},
},
},
)
)
session.commit()
filename, content = line_service.export_line_towers_to_csv(session, line=line)
assert filename.startswith("PL-LEGACY-001_towers_")
rows = list(csv.DictReader(io.StringIO(content.decode("utf-8-sig"))))
assert len(rows) == 1
row = rows[0]
assert row["序号"] == "1"
assert row["塔形"] == "直线塔"
assert row["I回相序"] == "ACB"
assert row["II回相序"] == "BAC"
assert row["III回相序"] == "CAB"
assert row["A相是否安装避雷器"] == ""
assert row["左避雷中距m"] == "8.8"
assert row["右避雷中距m"] == "9.9"
assert row["避雷线高度m"] == "41"
assert row["绝缘子串长度mm"] == "4200"
assert row["杆塔呼高m"] == "40.5"
assert row["I回上相中距m"] == "5.1"
assert row["III回上相中距m"] == "9.1"
assert row["电角度"] == "18"
assert row["雷电流幅值a"] == "31"
assert row["直线或耐张杆塔"] == "耐张杆塔"
assert row["绕击反击"] == "反击"
assert row["反击耐雷水平kA"] == "45"
assert row["绕击跳闸率(次/100km.a)"] == "0.3"
assert row["雷击风险等级"] == ""
assert row["几何参数JSON"].startswith("{")
assert row["雷电参数JSON"].startswith("{")
assert row["额外字段JSON"] == "{}"
finally:
session.close()
+95 -1
View File
@@ -1,18 +1,112 @@
from __future__ import annotations
from types import SimpleNamespace
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from app.core.database import Base
from app.models.line import Line
from app.models.line_tower import LineTower
from app.models.tower_profile import TowerProfile
from app.schemas.tower_profile import TowerProfileUpsertRequest
from app.services import tower_profile_service
def _build_session() -> Session:
engine = create_engine("sqlite+pysqlite:///:memory:")
Base.metadata.create_all(bind=engine, tables=[Line.__table__, LineTower.__table__, TowerProfile.__table__])
testing_session = sessionmaker(bind=engine, autocommit=False, autoflush=False, expire_on_commit=False)
return testing_session()
def test_tower_profile_upsert_request_accepts_new_professional_fields() -> None:
payload = TowerProfileUpsertRequest(
structure_kind="直线杆塔",
stroke_mode="反击",
protection_angle_left_deg=11.5,
protection_angle_right_deg=13.5,
shield_wire_height_m=41.0,
insulator_length_m=4200.0,
call_height_m=39.5,
angle_deg=18.0,
current_a=31.0,
current_b=2.6,
current_type="Heidler",
current_head_time_us=2.6,
current_tail_time_us=50.0,
geometry_layers_json={
"I": {
"phase_spacing_m": {"upper": 5.1, "middle": 4.2, "lower": 3.3},
"phase_height_m": {"upper": 25.0, "middle": 22.0, "lower": 19.0},
}
},
extra_profile_json={"cause_analysis": "接地电阻偏高"},
)
assert payload.structure_kind == "直线杆塔"
assert payload.stroke_mode == "反击"
assert payload.geometry_layers_json["I"]["phase_spacing_m"]["upper"] == 5.1
assert payload.protection_angle_left_deg == 11.5
assert payload.current_type == "Heidler"
assert payload.geometry_layers_json["I"]["phase_spacing_m"]["upper"] == 5.1
assert payload.extra_profile_json["cause_analysis"] == "接地电阻偏高"
def test_upsert_tower_profile_persists_professional_fields() -> None:
session = _build_session()
try:
line = Line(code="PL-TP-001", name="塔参线路", status="enabled")
session.add(line)
session.flush()
tower = LineTower(line_id=line.id, seq_no=1, tower_no="N-01")
session.add(tower)
session.commit()
payload = TowerProfileUpsertRequest(
structure_kind="耐张杆塔",
stroke_mode="反击",
phase_sequence_1="ABC",
arrester_a="",
protection_angle_left_deg=12.3,
protection_angle_right_deg=14.6,
shield_wire_height_m=40.2,
insulator_length_m=4100.0,
call_height_m=38.4,
angle_deg=16.5,
current_a=29.1,
current_b=2.4,
current_type="Heidler",
current_head_time_us=2.7,
current_tail_time_us=48.0,
geometry_layers_json={
"I": {
"phase_spacing_m": {"upper": 5.1, "middle": 4.0, "lower": 3.0},
"phase_height_m": {"upper": 29.0, "middle": 27.0, "lower": 25.0},
}
},
extra_profile_json={"cause_analysis": "接地电阻偏高"},
)
detail = tower_profile_service.upsert_tower_profile(
session,
tower.id,
payload,
actor=SimpleNamespace(id="tester"),
)
saved = tower_profile_service.get_tower_profile_by_tower_id(session, tower.id)
assert detail is not None
assert saved is not None
assert saved.structure_kind == "耐张杆塔"
assert saved.stroke_mode == "反击"
assert saved.protection_angle_left_deg == 12.3
assert saved.shield_wire_height_m == 40.2
assert saved.current_type == "Heidler"
assert saved.current_head_time_us == 2.7
assert saved.extra_profile_json["cause_analysis"] == "接地电阻偏高"
assert detail.geometry_layers_json["I"]["phase_spacing_m"]["upper"] == 5.1
assert detail.extra_profile_json["cause_analysis"] == "接地电阻偏高"
finally:
session.close()
+155 -34
View File
@@ -70,7 +70,19 @@ type TowerProfileFormValues = {
arrester_a: string;
arrester_b: string;
arrester_c: string;
protection_angle_left_deg: number | null;
protection_angle_right_deg: number | null;
shield_wire_height_m: number | null;
insulator_length_m: number | null;
call_height_m: number | null;
angle_deg: number | null;
current_a: number | null;
current_b: number | null;
current_type: string;
current_head_time_us: number | null;
current_tail_time_us: number | null;
geometry_layers_json: string;
extra_profile_json: string;
};
const TOWER_TYPE_OPTIONS = [
@@ -79,6 +91,11 @@ const TOWER_TYPE_OPTIONS = [
{ value: "耐张", label: "耐张" },
] as const;
const ARRESTER_INSTALL_OPTIONS = [
{ value: "是", label: "是" },
{ value: "否", label: "否" },
] as const;
const LINE_VOLTAGE_OPTIONS = [
{ value: "dc_500", label: "直流500kV", voltage_kv: 500 },
{ value: "dc_800", label: "直流800kV", voltage_kv: 800 },
@@ -170,7 +187,19 @@ const EMPTY_TOWER_PROFILE_FORM: TowerProfileFormValues = {
arrester_a: "",
arrester_b: "",
arrester_c: "",
protection_angle_left_deg: null,
protection_angle_right_deg: null,
shield_wire_height_m: null,
insulator_length_m: null,
call_height_m: null,
angle_deg: null,
current_a: null,
current_b: null,
current_type: "",
current_head_time_us: null,
current_tail_time_us: null,
geometry_layers_json: "{}",
extra_profile_json: "{}",
};
function resolveVoltageOptionFromKv(voltageKv: number | null): LineFormValues["voltage_level"] {
@@ -180,6 +209,25 @@ function resolveVoltageOptionFromKv(voltageKv: number | null): LineFormValues["v
return LINE_VOLTAGE_KV_TO_DEFAULT_OPTION[voltageKv] ?? null;
}
function formatJsonText(value: unknown): string {
if (!value || Array.isArray(value) || typeof value !== "object") {
return "{}";
}
return JSON.stringify(value, null, 2);
}
function parseJsonObjectText(value: string, label: string): Record<string, unknown> {
const normalized = value.trim();
if (!normalized) {
return {};
}
const parsed: unknown = JSON.parse(normalized);
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
throw new Error(`${label} 需要是 JSON 对象`);
}
return parsed as Record<string, unknown>;
}
export default function AdminPowerLinesPage() {
const { user, initializing, fetchWithAuth, hasPermission } = useAuth();
const queryClient = useQueryClient();
@@ -349,7 +397,19 @@ export default function AdminPowerLinesPage() {
arrester_a: profile.arrester_a ?? "",
arrester_b: profile.arrester_b ?? "",
arrester_c: profile.arrester_c ?? "",
geometry_layers_json: JSON.stringify(profile.geometry_layers_json ?? {}, null, 2),
protection_angle_left_deg: profile.protection_angle_left_deg ?? null,
protection_angle_right_deg: profile.protection_angle_right_deg ?? null,
shield_wire_height_m: profile.shield_wire_height_m ?? null,
insulator_length_m: profile.insulator_length_m ?? null,
call_height_m: profile.call_height_m ?? null,
angle_deg: profile.angle_deg ?? null,
current_a: profile.current_a ?? null,
current_b: profile.current_b ?? null,
current_type: profile.current_type ?? "",
current_head_time_us: profile.current_head_time_us ?? null,
current_tail_time_us: profile.current_tail_time_us ?? null,
geometry_layers_json: formatJsonText(profile.geometry_layers_json),
extra_profile_json: formatJsonText(profile.extra_profile_json),
});
}, [towerProfileForm, towerProfileQuery.data]);
@@ -583,23 +643,34 @@ export default function AdminPowerLinesPage() {
if (!editingTowerProfileTower) {
throw new Error("未选择杆塔");
}
const geometryLayers = values.geometry_layers_json.trim()
? JSON.parse(values.geometry_layers_json)
: {};
const geometryLayers = parseJsonObjectText(values.geometry_layers_json, "回路几何 JSON");
const extraProfile = parseJsonObjectText(values.extra_profile_json, "额外字段 JSON");
const response = await fetchWithAuth(`/api/v1/tower-profiles/${editingTowerProfileTower.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
structure_kind: values.structure_kind.trim() || null,
stroke_mode: values.stroke_mode.trim() || null,
phase_sequence_1: values.phase_sequence_1.trim() || null,
phase_sequence_2: values.phase_sequence_2.trim() || null,
phase_sequence_3: values.phase_sequence_3.trim() || null,
phase_sequence_4: values.phase_sequence_4.trim() || null,
arrester_a: values.arrester_a.trim() || null,
arrester_b: values.arrester_b.trim() || null,
arrester_c: values.arrester_c.trim() || null,
structure_kind: (values.structure_kind ?? "").trim() || null,
stroke_mode: (values.stroke_mode ?? "").trim() || null,
phase_sequence_1: (values.phase_sequence_1 ?? "").trim() || null,
phase_sequence_2: (values.phase_sequence_2 ?? "").trim() || null,
phase_sequence_3: (values.phase_sequence_3 ?? "").trim() || null,
phase_sequence_4: (values.phase_sequence_4 ?? "").trim() || null,
arrester_a: (values.arrester_a ?? "").trim() || null,
arrester_b: (values.arrester_b ?? "").trim() || null,
arrester_c: (values.arrester_c ?? "").trim() || null,
protection_angle_left_deg: values.protection_angle_left_deg ?? null,
protection_angle_right_deg: values.protection_angle_right_deg ?? null,
shield_wire_height_m: values.shield_wire_height_m ?? null,
insulator_length_m: values.insulator_length_m ?? null,
call_height_m: values.call_height_m ?? null,
angle_deg: values.angle_deg ?? null,
current_a: values.current_a ?? null,
current_b: values.current_b ?? null,
current_type: (values.current_type ?? "").trim() || null,
current_head_time_us: values.current_head_time_us ?? null,
current_tail_time_us: values.current_tail_time_us ?? null,
geometry_layers_json: geometryLayers,
extra_profile_json: extraProfile,
}),
});
if (!response.ok) {
@@ -1302,12 +1373,12 @@ export default function AdminPowerLinesPage() {
}}
>
<Form<TowerProfileFormValues> form={towerProfileForm} layout="vertical" initialValues={EMPTY_TOWER_PROFILE_FORM}>
<div className="grid gap-3 md:grid-cols-2">
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
<Form.Item name="structure_kind" label="直线/耐张">
<Input />
<Input placeholder="如:直线、耐张、直线杆塔" />
</Form.Item>
<Form.Item name="stroke_mode" label="绕击/反击模式">
<Input />
<Input placeholder="如:绕击、反击" />
</Form.Item>
<Form.Item name="phase_sequence_1" label="I回相序">
<Input />
@@ -1322,30 +1393,80 @@ export default function AdminPowerLinesPage() {
<Input />
</Form.Item>
<Form.Item name="arrester_a" label="A相避雷器">
<Input />
<Select allowClear options={ARRESTER_INSTALL_OPTIONS} />
</Form.Item>
<Form.Item name="arrester_b" label="B相避雷器">
<Input />
<Select allowClear options={ARRESTER_INSTALL_OPTIONS} />
</Form.Item>
<Form.Item name="arrester_c" label="C相避雷器">
<Input />
<Select allowClear options={ARRESTER_INSTALL_OPTIONS} />
</Form.Item>
<Form.Item name="protection_angle_left_deg" label="左保护角">
<InputNumber className="w-full" precision={4} />
</Form.Item>
<Form.Item name="protection_angle_right_deg" label="右保护角">
<InputNumber className="w-full" precision={4} />
</Form.Item>
<Form.Item name="shield_wire_height_m" label="避雷线高度(m)">
<InputNumber className="w-full" precision={4} />
</Form.Item>
<Form.Item name="insulator_length_m" label="绝缘子串长度(mm)">
<InputNumber className="w-full" precision={4} />
</Form.Item>
<Form.Item name="call_height_m" label="杆塔呼高(m)">
<InputNumber className="w-full" precision={4} />
</Form.Item>
<Form.Item name="angle_deg" label="电角度">
<InputNumber className="w-full" precision={4} />
</Form.Item>
<Form.Item name="current_a" label="雷电流幅值 a">
<InputNumber className="w-full" precision={4} />
</Form.Item>
<Form.Item name="current_b" label="雷电流幅值 b">
<InputNumber className="w-full" precision={4} />
</Form.Item>
<Form.Item name="current_type" label="雷电流波形">
<Input placeholder="如:Heidler、双指数" />
</Form.Item>
<Form.Item name="current_head_time_us" label="波头时间(us)">
<InputNumber className="w-full" precision={4} />
</Form.Item>
<Form.Item name="current_tail_time_us" label="波尾时间(us)">
<InputNumber className="w-full" precision={4} />
</Form.Item>
</div>
<div className="grid gap-3 xl:grid-cols-2">
<Form.Item
name="geometry_layers_json"
label="回路几何 JSON"
rules={[{
validator: async (_, value) => {
if (!value || !String(value).trim()) {
return;
}
parseJsonObjectText(String(value), "回路几何 JSON");
},
message: "请输入合法 JSON 对象",
}]}
>
<Input.TextArea rows={12} spellCheck={false} />
</Form.Item>
<Form.Item
name="extra_profile_json"
label="额外字段 JSON"
rules={[{
validator: async (_, value) => {
if (!value || !String(value).trim()) {
return;
}
parseJsonObjectText(String(value), "额外字段 JSON");
},
message: "请输入合法 JSON 对象",
}]}
>
<Input.TextArea rows={12} spellCheck={false} />
</Form.Item>
</div>
<Form.Item
name="geometry_layers_json"
label="回路几何 JSON"
rules={[{
validator: async (_, value) => {
if (!value || !String(value).trim()) {
return;
}
JSON.parse(String(value));
},
message: "请输入合法 JSON",
}]}
>
<Input.TextArea rows={12} spellCheck={false} />
</Form.Item>
</Form>
</Modal>
</>