feat: migrate tower profile professional fields
This commit is contained in:
@@ -13,6 +13,7 @@ from .v1.question_bank import router as question_bank_router
|
||||
from .v1.system_params import router as system_params_router
|
||||
from .v1.task_monitor import router as task_monitor_router
|
||||
from .v1.tower_models import router as tower_models_router
|
||||
from .v1.tower_profiles import router as tower_profiles_router
|
||||
from .v1.users import router as users_router
|
||||
from .v1.wine import router as wine_router
|
||||
from .v1.ws import router as ws_router
|
||||
@@ -31,6 +32,7 @@ v1_router.include_router(flower_monitor_router)
|
||||
v1_router.include_router(lightning_router)
|
||||
v1_router.include_router(lines_router)
|
||||
v1_router.include_router(tower_models_router)
|
||||
v1_router.include_router(tower_profiles_router)
|
||||
v1_router.include_router(question_bank_router)
|
||||
v1_router.include_router(wine_router)
|
||||
v1_router.include_router(ws_router)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ...core.database import get_db
|
||||
from ...core.dependencies import CurrentUser, require_any_permission, require_permission
|
||||
from ...schemas.tower_profile import TowerProfileDetail, TowerProfileUpsertRequest
|
||||
from ...services.tower_profile_service import get_tower_profile_detail, upsert_tower_profile
|
||||
|
||||
router = APIRouter(prefix="/tower-profiles", tags=["tower-profiles"])
|
||||
|
||||
|
||||
@router.get("/{tower_id}", response_model=TowerProfileDetail)
|
||||
def get_tower_profile_endpoint(
|
||||
tower_id: str,
|
||||
_: CurrentUser = Depends(require_any_permission("tower.read", "tower.manage", "line.read", "line.manage")),
|
||||
db: Session = Depends(get_db),
|
||||
) -> TowerProfileDetail:
|
||||
item = get_tower_profile_detail(db, tower_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tower not found")
|
||||
return item
|
||||
|
||||
|
||||
@router.put("/{tower_id}", response_model=TowerProfileDetail)
|
||||
def put_tower_profile_endpoint(
|
||||
tower_id: str,
|
||||
payload: TowerProfileUpsertRequest,
|
||||
current_user: CurrentUser = Depends(require_permission("tower.manage")),
|
||||
db: Session = Depends(get_db),
|
||||
) -> TowerProfileDetail:
|
||||
item = upsert_tower_profile(db, tower_id, payload, actor=current_user.user)
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tower not found")
|
||||
return item
|
||||
@@ -332,6 +332,41 @@ def _ensure_tower_model_column_compatibility() -> None:
|
||||
)
|
||||
|
||||
|
||||
def _ensure_tower_profile_column_compatibility() -> None:
|
||||
"""
|
||||
Keep `tower_profile` columns aligned with the current ORM mapping.
|
||||
"""
|
||||
if not database_url.startswith("postgresql"):
|
||||
return
|
||||
|
||||
schema = settings.resolved_db_schema
|
||||
with engine.begin() as connection:
|
||||
db_inspector = inspect(connection)
|
||||
if not db_inspector.has_table("tower_profile", schema=schema):
|
||||
return
|
||||
|
||||
column_names = {
|
||||
column["name"]
|
||||
for column in db_inspector.get_columns("tower_profile", schema=schema)
|
||||
}
|
||||
|
||||
if "structure_kind" not in column_names:
|
||||
connection.execute(
|
||||
text("ALTER TABLE tower_profile ADD COLUMN IF NOT EXISTS structure_kind VARCHAR(64)"),
|
||||
)
|
||||
logger.warning(
|
||||
"Detected missing tower_profile.structure_kind; added nullable structure kind column.",
|
||||
)
|
||||
|
||||
if "stroke_mode" not in column_names:
|
||||
connection.execute(
|
||||
text("ALTER TABLE tower_profile ADD COLUMN IF NOT EXISTS stroke_mode VARCHAR(32)"),
|
||||
)
|
||||
logger.warning(
|
||||
"Detected missing tower_profile.stroke_mode; added nullable stroke mode column.",
|
||||
)
|
||||
|
||||
|
||||
def get_db() -> Generator[Session, None, None]:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@@ -375,6 +410,7 @@ def init_db() -> None:
|
||||
_ensure_user_audit_column_compatibility()
|
||||
_ensure_elevation_dataset_column_compatibility()
|
||||
_ensure_tower_model_column_compatibility()
|
||||
_ensure_tower_profile_column_compatibility()
|
||||
Base.metadata.create_all(bind=engine)
|
||||
with SessionLocal() as db:
|
||||
local_hosts = {"db", "localhost", "127.0.0.1", "::1"}
|
||||
|
||||
@@ -48,6 +48,8 @@ class TowerProfile(Base):
|
||||
angle_deg: Mapped[float | None] = mapped_column(Float)
|
||||
current_a: Mapped[float | None] = mapped_column(Float)
|
||||
current_b: Mapped[float | None] = mapped_column(Float)
|
||||
structure_kind: Mapped[str | None] = mapped_column(String(64), index=True)
|
||||
stroke_mode: Mapped[str | None] = mapped_column(String(32), index=True)
|
||||
current_type: Mapped[str | None] = mapped_column(String(32), index=True)
|
||||
current_head_time_us: Mapped[float | None] = mapped_column(Float)
|
||||
current_tail_time_us: Mapped[float | None] = mapped_column(Float)
|
||||
|
||||
@@ -30,6 +30,8 @@ class TowerProfileDetail(BaseModel):
|
||||
angle_deg: float | None = None
|
||||
current_a: float | None = None
|
||||
current_b: float | None = None
|
||||
structure_kind: str | None = None
|
||||
stroke_mode: str | None = None
|
||||
current_type: str | None = None
|
||||
current_head_time_us: float | None = None
|
||||
current_tail_time_us: float | None = None
|
||||
@@ -57,6 +59,8 @@ class TowerProfileUpsertRequest(BaseModel):
|
||||
angle_deg: float | None = None
|
||||
current_a: float | None = None
|
||||
current_b: float | None = None
|
||||
structure_kind: str | None = Field(default=None, max_length=64)
|
||||
stroke_mode: str | None = Field(default=None, max_length=32)
|
||||
current_type: str | None = Field(default=None, max_length=32)
|
||||
current_head_time_us: float | None = None
|
||||
current_tail_time_us: float | None = None
|
||||
|
||||
@@ -15,6 +15,7 @@ from ..models.base import utcnow
|
||||
from ..models.line import Line
|
||||
from ..models.line_tower import LineTower
|
||||
from ..models.tower_model import TowerModel
|
||||
from ..models.tower_profile import TowerProfile
|
||||
from ..schemas.line import (
|
||||
LineCreateRequest,
|
||||
LineListResponse,
|
||||
@@ -506,6 +507,13 @@ def import_line_towers_from_csv(
|
||||
tower.raw_extra_json = _extract_extra_values(row, extra_headers)
|
||||
tower.update_user = actor_user_id
|
||||
tower.update_date = utcnow()
|
||||
db.flush()
|
||||
_upsert_tower_profile_from_legacy_row(
|
||||
db,
|
||||
tower=tower,
|
||||
row=row,
|
||||
actor_user_id=actor_user_id,
|
||||
)
|
||||
|
||||
tower_by_seq[tower.seq_no] = tower
|
||||
tower_by_no[tower.tower_no] = tower
|
||||
@@ -744,6 +752,58 @@ def _build_lightning_result(row: dict[str, str]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _upsert_tower_profile_from_legacy_row(
|
||||
db: Session,
|
||||
*,
|
||||
tower: LineTower,
|
||||
row: dict[str, str],
|
||||
actor_user_id: str,
|
||||
) -> None:
|
||||
profile = db.execute(select(TowerProfile).where(TowerProfile.tower_id == tower.id)).scalar_one_or_none()
|
||||
now = utcnow()
|
||||
if profile is None:
|
||||
profile = TowerProfile(
|
||||
tower_id=tower.id,
|
||||
create_date=now,
|
||||
create_user=actor_user_id,
|
||||
update_date=now,
|
||||
update_user=actor_user_id,
|
||||
)
|
||||
db.add(profile)
|
||||
|
||||
geometry_layers = _build_circuit_geometry(row)
|
||||
extra_profile_json = dict(profile.extra_profile_json or {})
|
||||
|
||||
profile.phase_sequence_1 = _pick_optional_value(_normalize_str(row.get("I回相序")), profile.phase_sequence_1)
|
||||
profile.phase_sequence_2 = _pick_optional_value(_normalize_str(row.get("II回相序")), profile.phase_sequence_2)
|
||||
profile.phase_sequence_3 = _pick_optional_value(_normalize_str(row.get("III回相序")), profile.phase_sequence_3)
|
||||
profile.phase_sequence_4 = _pick_optional_value(_normalize_str(row.get("IV回相序")), profile.phase_sequence_4)
|
||||
profile.arrester_a = _pick_optional_value(_normalize_str(row.get("A相是否安装避雷器")), profile.arrester_a)
|
||||
profile.arrester_b = _pick_optional_value(_normalize_str(row.get("B相是否安装避雷器")), profile.arrester_b)
|
||||
profile.arrester_c = _pick_optional_value(_normalize_str(row.get("C相是否安装避雷器")), profile.arrester_c)
|
||||
profile.protection_angle_left_deg = _pick_optional_value(_parse_float(row.get("左避雷中距m")), profile.protection_angle_left_deg)
|
||||
profile.protection_angle_right_deg = _pick_optional_value(_parse_float(row.get("右避雷中距m")), profile.protection_angle_right_deg)
|
||||
profile.shield_wire_height_m = _pick_optional_value(_parse_float(row.get("避雷线高度m")), profile.shield_wire_height_m)
|
||||
profile.insulator_length_m = _pick_optional_value(_parse_float(row.get("绝缘子串长度mm")), profile.insulator_length_m)
|
||||
profile.call_height_m = _pick_optional_value(_parse_float(row.get("杆塔呼高m")), profile.call_height_m)
|
||||
profile.angle_deg = _pick_optional_value(_parse_float(row.get("电角度")), profile.angle_deg)
|
||||
profile.current_a = _pick_optional_value(_parse_float(row.get("雷电流幅值a")), profile.current_a)
|
||||
profile.current_b = _pick_optional_value(_parse_float(row.get("雷电流幅值b")), profile.current_b)
|
||||
profile.structure_kind = _pick_optional_value(_normalize_str(row.get("直线或耐张杆塔")), profile.structure_kind)
|
||||
profile.stroke_mode = _pick_optional_value(_normalize_str(row.get("绕击反击")), profile.stroke_mode)
|
||||
profile.geometry_layers_json = _pick_dict_value(geometry_layers, profile.geometry_layers_json or {})
|
||||
|
||||
cause_analysis = _normalize_str(row.get("原因分析"))
|
||||
mitigation_recommendation = _normalize_str(row.get("措施推荐"))
|
||||
if cause_analysis is not None:
|
||||
extra_profile_json["cause_analysis"] = cause_analysis
|
||||
if mitigation_recommendation is not None:
|
||||
extra_profile_json["mitigation_recommendation"] = mitigation_recommendation
|
||||
profile.extra_profile_json = extra_profile_json
|
||||
profile.update_date = now
|
||||
profile.update_user = actor_user_id
|
||||
|
||||
|
||||
def _extract_extra_values(row: dict[str, str], extra_headers: list[str]) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {}
|
||||
for key in extra_headers:
|
||||
|
||||
@@ -43,6 +43,8 @@ def serialize_tower_profile(tower: LineTower, profile: TowerProfile | None) -> T
|
||||
angle_deg=profile.angle_deg if profile else None,
|
||||
current_a=profile.current_a if profile else None,
|
||||
current_b=profile.current_b if profile else None,
|
||||
structure_kind=profile.structure_kind if profile else None,
|
||||
stroke_mode=profile.stroke_mode if profile else None,
|
||||
current_type=profile.current_type if profile else None,
|
||||
current_head_time_us=profile.current_head_time_us if profile else None,
|
||||
current_tail_time_us=profile.current_tail_time_us if profile else None,
|
||||
@@ -101,6 +103,8 @@ def upsert_tower_profile(
|
||||
profile.angle_deg = payload.angle_deg
|
||||
profile.current_a = payload.current_a
|
||||
profile.current_b = payload.current_b
|
||||
profile.structure_kind = payload.structure_kind
|
||||
profile.stroke_mode = payload.stroke_mode
|
||||
profile.current_type = payload.current_type
|
||||
profile.current_head_time_us = payload.current_head_time_us
|
||||
profile.current_tail_time_us = payload.current_tail_time_us
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
from app.schemas.tower_profile import TowerProfileUpsertRequest
|
||||
|
||||
|
||||
def test_tower_profile_upsert_request_accepts_new_professional_fields() -> None:
|
||||
payload = TowerProfileUpsertRequest(
|
||||
structure_kind="直线杆塔",
|
||||
stroke_mode="反击",
|
||||
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},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
assert payload.structure_kind == "直线杆塔"
|
||||
assert payload.stroke_mode == "反击"
|
||||
assert payload.geometry_layers_json["I"]["phase_spacing_m"]["upper"] == 5.1
|
||||
@@ -36,6 +36,7 @@ import type {
|
||||
LineTowerListResponse,
|
||||
LineTowerSummary,
|
||||
TowerModelSummary,
|
||||
TowerProfileDetail,
|
||||
} from "@/types/auth";
|
||||
|
||||
type LineFormValues = {
|
||||
@@ -64,6 +65,19 @@ type TowerFormValues = {
|
||||
risk_level: string;
|
||||
};
|
||||
|
||||
type TowerProfileFormValues = {
|
||||
structure_kind: string;
|
||||
stroke_mode: string;
|
||||
phase_sequence_1: string;
|
||||
phase_sequence_2: string;
|
||||
phase_sequence_3: string;
|
||||
phase_sequence_4: string;
|
||||
arrester_a: string;
|
||||
arrester_b: string;
|
||||
arrester_c: string;
|
||||
geometry_layers_json: string;
|
||||
};
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: "all", label: "全部状态" },
|
||||
{ value: "enabled", label: "启用" },
|
||||
@@ -165,6 +179,19 @@ const EMPTY_TOWER_FORM: TowerFormValues = {
|
||||
risk_level: "",
|
||||
};
|
||||
|
||||
const EMPTY_TOWER_PROFILE_FORM: TowerProfileFormValues = {
|
||||
structure_kind: "",
|
||||
stroke_mode: "",
|
||||
phase_sequence_1: "",
|
||||
phase_sequence_2: "",
|
||||
phase_sequence_3: "",
|
||||
phase_sequence_4: "",
|
||||
arrester_a: "",
|
||||
arrester_b: "",
|
||||
arrester_c: "",
|
||||
geometry_layers_json: "{}",
|
||||
};
|
||||
|
||||
function formatStatus(status: string): string {
|
||||
if (status === "enabled") return "启用";
|
||||
if (status === "disabled") return "禁用";
|
||||
@@ -186,6 +213,7 @@ export default function AdminPowerLinesPage() {
|
||||
const panelScrollAnchorRef = useRef<HTMLDivElement | null>(null);
|
||||
const [lineForm] = Form.useForm<LineFormValues>();
|
||||
const [towerForm] = Form.useForm<TowerFormValues>();
|
||||
const [towerProfileForm] = Form.useForm<TowerProfileFormValues>();
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<(typeof STATUS_OPTIONS)[number]["value"]>("all");
|
||||
@@ -197,8 +225,10 @@ export default function AdminPowerLinesPage() {
|
||||
const [towerPagination, setTowerPagination] = useState({ current: 1, pageSize: TOWER_TABLE_DEFAULT_PAGE_SIZE });
|
||||
const [lineModalOpen, setLineModalOpen] = useState(false);
|
||||
const [towerModalOpen, setTowerModalOpen] = useState(false);
|
||||
const [towerProfileModalOpen, setTowerProfileModalOpen] = useState(false);
|
||||
const [editingLine, setEditingLine] = useState<LineSummary | null>(null);
|
||||
const [editingTower, setEditingTower] = useState<LineTowerSummary | null>(null);
|
||||
const [editingTowerProfileTower, setEditingTowerProfileTower] = useState<LineTowerSummary | null>(null);
|
||||
const [towerViewMode, setTowerViewMode] = useState<"table" | "map">("map");
|
||||
const [error, setError] = useState("");
|
||||
const [panelBodyHeight, setPanelBodyHeight] = useState(POWER_LINES_PANEL_MIN_HEIGHT);
|
||||
@@ -293,6 +323,18 @@ export default function AdminPowerLinesPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const towerProfileQuery = useQuery({
|
||||
queryKey: ["tower-profile", editingTowerProfileTower?.id],
|
||||
enabled: !!user && !!editingTowerProfileTower && towerProfileModalOpen && canTowerRead,
|
||||
queryFn: async () => {
|
||||
const response = await fetchWithAuth(`/api/v1/tower-profiles/${editingTowerProfileTower?.id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(await readApiError(response));
|
||||
}
|
||||
return (await response.json()) as TowerProfileDetail;
|
||||
},
|
||||
});
|
||||
|
||||
const refreshLines = useCallback(async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
predicate: (query) =>
|
||||
@@ -311,6 +353,25 @@ export default function AdminPowerLinesPage() {
|
||||
});
|
||||
}, [queryClient]);
|
||||
|
||||
useEffect(() => {
|
||||
const profile = towerProfileQuery.data;
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
towerProfileForm.setFieldsValue({
|
||||
structure_kind: profile.structure_kind ?? "",
|
||||
stroke_mode: profile.stroke_mode ?? "",
|
||||
phase_sequence_1: profile.phase_sequence_1 ?? "",
|
||||
phase_sequence_2: profile.phase_sequence_2 ?? "",
|
||||
phase_sequence_3: profile.phase_sequence_3 ?? "",
|
||||
phase_sequence_4: profile.phase_sequence_4 ?? "",
|
||||
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),
|
||||
});
|
||||
}, [towerProfileForm, towerProfileQuery.data]);
|
||||
|
||||
useTopicSubscription("admin.power-lines", useCallback(() => {
|
||||
void refreshLines();
|
||||
void refreshTowers();
|
||||
@@ -531,6 +592,48 @@ export default function AdminPowerLinesPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const saveTowerProfileMutation = useMutation({
|
||||
mutationFn: async (values: TowerProfileFormValues) => {
|
||||
if (!editingTowerProfileTower) {
|
||||
throw new Error("未选择杆塔");
|
||||
}
|
||||
const geometryLayers = values.geometry_layers_json.trim()
|
||||
? JSON.parse(values.geometry_layers_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,
|
||||
geometry_layers_json: geometryLayers,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(await readApiError(response));
|
||||
}
|
||||
return (await response.json()) as TowerProfileDetail;
|
||||
},
|
||||
onSuccess: async () => {
|
||||
setError("");
|
||||
messageApi.success("专业参数已保存");
|
||||
setTowerProfileModalOpen(false);
|
||||
setEditingTowerProfileTower(null);
|
||||
towerProfileForm.resetFields();
|
||||
await queryClient.invalidateQueries({ queryKey: ["tower-profile"] });
|
||||
},
|
||||
onError: (candidate) => {
|
||||
setError(candidate instanceof Error ? candidate.message : "保存专业参数失败");
|
||||
},
|
||||
});
|
||||
|
||||
const importMutation = useMutation({
|
||||
mutationFn: async (file: File) => {
|
||||
if (!effectiveSelectedLineId) {
|
||||
@@ -646,6 +749,12 @@ export default function AdminPowerLinesPage() {
|
||||
setTowerModalOpen(true);
|
||||
};
|
||||
|
||||
const openTowerProfileModal = (item: LineTowerSummary) => {
|
||||
setEditingTowerProfileTower(item);
|
||||
towerProfileForm.setFieldsValue(EMPTY_TOWER_PROFILE_FORM);
|
||||
setTowerProfileModalOpen(true);
|
||||
};
|
||||
|
||||
const lineCards = lines.map((line) => {
|
||||
const selected = line.id === effectiveSelectedLineId;
|
||||
return (
|
||||
@@ -757,6 +866,11 @@ export default function AdminPowerLinesPage() {
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
{canTowerManage && (
|
||||
<Button size="small" onClick={() => openTowerProfileModal(row)}>
|
||||
专业参数
|
||||
</Button>
|
||||
)}
|
||||
{canTowerManage && (
|
||||
<Popconfirm
|
||||
title="删除杆塔"
|
||||
@@ -1199,6 +1313,70 @@ export default function AdminPowerLinesPage() {
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={editingTowerProfileTower ? `专业参数 - ${editingTowerProfileTower.tower_no}` : "专业参数"}
|
||||
open={towerProfileModalOpen}
|
||||
width={920}
|
||||
okText="保存"
|
||||
confirmLoading={saveTowerProfileMutation.isPending}
|
||||
onCancel={() => {
|
||||
if (saveTowerProfileMutation.isPending) return;
|
||||
setTowerProfileModalOpen(false);
|
||||
setEditingTowerProfileTower(null);
|
||||
}}
|
||||
onOk={async () => {
|
||||
const values = await towerProfileForm.validateFields();
|
||||
saveTowerProfileMutation.mutate(values);
|
||||
}}
|
||||
>
|
||||
<Form<TowerProfileFormValues> form={towerProfileForm} layout="vertical" initialValues={EMPTY_TOWER_PROFILE_FORM}>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<Form.Item name="structure_kind" label="直线/耐张">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="stroke_mode" label="绕击/反击模式">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="phase_sequence_1" label="I回相序">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="phase_sequence_2" label="II回相序">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="phase_sequence_3" label="III回相序">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="phase_sequence_4" label="IV回相序">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="arrester_a" label="A相避雷器">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="arrester_b" label="B相避雷器">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="arrester_c" label="C相避雷器">
|
||||
<Input />
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -578,6 +578,43 @@ export type LineTowerImportResponse = {
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
export type TowerProfileDetail = {
|
||||
id: string | null;
|
||||
tower_id: string;
|
||||
line_id: string;
|
||||
tower_no: string;
|
||||
seq_no: number;
|
||||
tower_model: string | null;
|
||||
tower_type: string | null;
|
||||
profile_exists: boolean;
|
||||
phase_sequence_1: string | null;
|
||||
phase_sequence_2: string | null;
|
||||
phase_sequence_3: string | null;
|
||||
phase_sequence_4: string | null;
|
||||
arrester_a: string | null;
|
||||
arrester_b: string | null;
|
||||
arrester_c: string | null;
|
||||
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;
|
||||
structure_kind: string | null;
|
||||
stroke_mode: string | null;
|
||||
current_type: string | null;
|
||||
current_head_time_us: number | null;
|
||||
current_tail_time_us: number | null;
|
||||
geometry_layers_json: Record<string, unknown>;
|
||||
extra_profile_json: Record<string, unknown>;
|
||||
create_date: string | null;
|
||||
create_user: string | null;
|
||||
update_date: string | null;
|
||||
update_user: string | null;
|
||||
};
|
||||
|
||||
export type AtpModelStatus = "enabled" | "disabled";
|
||||
export type AtpModelSourceType = "atpdraw" | "atp" | "manual";
|
||||
export type AtpModelVersionStatus = "draft" | "released" | "archived";
|
||||
|
||||
Reference in New Issue
Block a user