feat(map): add valve setting editor
This commit is contained in:
@@ -1,9 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useRef } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import Draggable from "react-draggable";
|
import Draggable from "react-draggable";
|
||||||
import { Close } from "@mui/icons-material";
|
import { Close } from "@mui/icons-material";
|
||||||
import { IconButton, Tooltip } from "@mui/material";
|
import {
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
TextField,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mui/material";
|
||||||
|
import type { SelectChangeEvent } from "@mui/material/Select";
|
||||||
|
|
||||||
interface BaseProperty {
|
interface BaseProperty {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -20,7 +30,29 @@ interface TableProperty {
|
|||||||
rows: (string | number)[][]; // 每行的数据
|
rows: (string | number)[][]; // 每行的数据
|
||||||
}
|
}
|
||||||
|
|
||||||
type PropertyItem = BaseProperty | TableProperty;
|
interface SelectProperty {
|
||||||
|
type: "select";
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
options: { label: string; value: string }[];
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
saving?: boolean;
|
||||||
|
onSave: (value: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TextProperty {
|
||||||
|
type: "text";
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
saving?: boolean;
|
||||||
|
helperText?: string;
|
||||||
|
onSave: (value: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PropertyItem = BaseProperty | TableProperty | SelectProperty | TextProperty;
|
||||||
|
|
||||||
interface PropertyPanelProps {
|
interface PropertyPanelProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -36,6 +68,7 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const draggableRef = useRef<HTMLDivElement>(null);
|
const draggableRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [draftValues, setDraftValues] = useState<Record<string, string>>({});
|
||||||
const headerActionSx = {
|
const headerActionSx = {
|
||||||
color: "common.white",
|
color: "common.white",
|
||||||
backgroundColor: "rgba(255,255,255,0.08)",
|
backgroundColor: "rgba(255,255,255,0.08)",
|
||||||
@@ -56,6 +89,26 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||||||
|
|
||||||
const isImportantKeys = ["ID", "类型", "Name", "面积", "长度"];
|
const isImportantKeys = ["ID", "类型", "Name", "面积", "长度"];
|
||||||
|
|
||||||
|
const handleSelectChange = (
|
||||||
|
key: string,
|
||||||
|
event: SelectChangeEvent,
|
||||||
|
) => {
|
||||||
|
setDraftValues((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: event.target.value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTextChange = (
|
||||||
|
key: string,
|
||||||
|
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
|
) => {
|
||||||
|
setDraftValues((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: event.target.value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
// 统计属性数量(表格型按行数计入)
|
// 统计属性数量(表格型按行数计入)
|
||||||
const totalProps = id
|
const totalProps = id
|
||||||
? 2 +
|
? 2 +
|
||||||
@@ -200,6 +253,151 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("type" in property && property.type === "select") {
|
||||||
|
const selected = property as SelectProperty;
|
||||||
|
const draftKey = `${id ?? "unknown"}:${selected.label}`;
|
||||||
|
const draftValue = draftValues[draftKey] ?? selected.value ?? "";
|
||||||
|
const hasChanged = draftValue !== selected.value;
|
||||||
|
const getOptionLabel = (value: string) =>
|
||||||
|
selected.options.find((option) => option.value === value)
|
||||||
|
?.label ?? value;
|
||||||
|
const canSave =
|
||||||
|
hasChanged &&
|
||||||
|
draftValue !== "" &&
|
||||||
|
!selected.disabled &&
|
||||||
|
!selected.saving;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`select-${index}`}
|
||||||
|
className="group rounded-lg p-3 transition-all duration-200 bg-gray-50 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<span className="font-medium text-xs uppercase tracking-wide text-gray-600">
|
||||||
|
{selected.label}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center justify-end gap-2 flex-1">
|
||||||
|
<FormControl size="small" sx={{ minWidth: 118 }}>
|
||||||
|
<Select
|
||||||
|
value={draftValue}
|
||||||
|
displayEmpty
|
||||||
|
disabled={selected.disabled || selected.saving}
|
||||||
|
onChange={(event) =>
|
||||||
|
handleSelectChange(draftKey, event)
|
||||||
|
}
|
||||||
|
renderValue={(value) =>
|
||||||
|
value
|
||||||
|
? getOptionLabel(String(value))
|
||||||
|
: selected.placeholder || "-"
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
height: 32,
|
||||||
|
fontSize: 13,
|
||||||
|
backgroundColor: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem value="" disabled>
|
||||||
|
{selected.placeholder || "未设置"}
|
||||||
|
</MenuItem>
|
||||||
|
{selected.options.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
disabled={!canSave}
|
||||||
|
onClick={() => selected.onSave(draftValue)}
|
||||||
|
sx={{
|
||||||
|
minWidth: 58,
|
||||||
|
height: 32,
|
||||||
|
fontSize: 13,
|
||||||
|
boxShadow: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selected.saving ? (
|
||||||
|
<CircularProgress size={16} color="inherit" />
|
||||||
|
) : (
|
||||||
|
"保存"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("type" in property && property.type === "text") {
|
||||||
|
const editable = property as TextProperty;
|
||||||
|
const draftKey = `${id ?? "unknown"}:${editable.label}`;
|
||||||
|
const draftValue = draftValues[draftKey] ?? editable.value ?? "";
|
||||||
|
const hasChanged = draftValue !== editable.value;
|
||||||
|
const canSave =
|
||||||
|
hasChanged &&
|
||||||
|
!editable.disabled &&
|
||||||
|
!editable.saving;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`text-${index}`}
|
||||||
|
className="group rounded-lg p-3 transition-all duration-200 bg-gray-50 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<span className="font-medium text-xs uppercase tracking-wide text-gray-600 pt-2">
|
||||||
|
{editable.label}
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-col items-end gap-1 flex-1">
|
||||||
|
<div className="flex items-center justify-end gap-2 w-full">
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
value={draftValue}
|
||||||
|
placeholder={editable.placeholder}
|
||||||
|
disabled={editable.disabled || editable.saving}
|
||||||
|
onChange={(event) =>
|
||||||
|
handleTextChange(draftKey, event)
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
width: 150,
|
||||||
|
"& .MuiInputBase-input": {
|
||||||
|
height: 15,
|
||||||
|
fontSize: 13,
|
||||||
|
},
|
||||||
|
backgroundColor: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
disabled={!canSave}
|
||||||
|
onClick={() => editable.onSave(draftValue)}
|
||||||
|
sx={{
|
||||||
|
minWidth: 58,
|
||||||
|
height: 32,
|
||||||
|
fontSize: 13,
|
||||||
|
boxShadow: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{editable.saving ? (
|
||||||
|
<CircularProgress size={16} color="inherit" />
|
||||||
|
) : (
|
||||||
|
"保存"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{editable.helperText && (
|
||||||
|
<span className="text-xs text-gray-500 text-right">
|
||||||
|
{editable.helperText}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 普通属性
|
// 普通属性
|
||||||
const base = property as BaseProperty;
|
const base = property as BaseProperty;
|
||||||
const isImportant = isImportantKeys.includes(base.label);
|
const isImportant = isImportantKeys.includes(base.label);
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import {
|
|||||||
import { useToolbarChatActions } from "./useToolbarChatActions";
|
import { useToolbarChatActions } from "./useToolbarChatActions";
|
||||||
import { useStyleEditor } from "./useStyleEditor";
|
import { useStyleEditor } from "./useStyleEditor";
|
||||||
|
|
||||||
import { config } from "@/config/config";
|
import { config, NETWORK_NAME } from "@/config/config";
|
||||||
|
import { useProject } from "@/contexts/ProjectContext";
|
||||||
import { apiFetch } from "@/lib/apiFetch";
|
import { apiFetch } from "@/lib/apiFetch";
|
||||||
|
|
||||||
// 添加接口定义隐藏按钮的props
|
// 添加接口定义隐藏按钮的props
|
||||||
@@ -37,6 +38,52 @@ interface ToolbarProps {
|
|||||||
HistoryPanel?: React.FC<any>; // 可选的自定义历史数据面板
|
HistoryPanel?: React.FC<any>; // 可选的自定义历史数据面板
|
||||||
enableCompare?: boolean;
|
enableCompare?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LinkStatus = "OPEN" | "CLOSED" | "ACTIVE";
|
||||||
|
type ValveProperties = {
|
||||||
|
vType: string | null;
|
||||||
|
setting: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValveLayer = (layerId: string | undefined) =>
|
||||||
|
layerId === "geo_valves_mat" || layerId === "geo_valves";
|
||||||
|
|
||||||
|
const isLinkStatus = (value: unknown): value is LinkStatus =>
|
||||||
|
value === "OPEN" || value === "CLOSED" || value === "ACTIVE";
|
||||||
|
|
||||||
|
const normalizeValveSetting = (value: unknown): string | null => {
|
||||||
|
if (value === undefined || value === null) return null;
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateValveSetting = (
|
||||||
|
valveType: string | null,
|
||||||
|
value: string,
|
||||||
|
): string | null => {
|
||||||
|
const normalizedType = valveType?.toUpperCase();
|
||||||
|
const trimmedValue = value.trim();
|
||||||
|
const numericTypes = new Set(["PRV", "PSV", "PBV", "FCV", "TCV"]);
|
||||||
|
|
||||||
|
if (normalizedType === "GPV") {
|
||||||
|
return trimmedValue ? null : "GPV 阀门设置值必须是非空曲线 ID。";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numericTypes.has(normalizedType ?? "")) {
|
||||||
|
if (!trimmedValue) {
|
||||||
|
return "阀门设置值必须是 0 或正数。";
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericValue = Number(trimmedValue);
|
||||||
|
if (!Number.isFinite(numericValue) || numericValue < 0) {
|
||||||
|
return "阀门设置值必须是有限数字,且大于或等于 0。";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmedValue ? null : "阀门设置值不能为空。";
|
||||||
|
};
|
||||||
|
|
||||||
const Toolbar: React.FC<ToolbarProps> = ({
|
const Toolbar: React.FC<ToolbarProps> = ({
|
||||||
hiddenButtons,
|
hiddenButtons,
|
||||||
queryType,
|
queryType,
|
||||||
@@ -46,6 +93,7 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
const data = useData();
|
const data = useData();
|
||||||
|
const project = useProject();
|
||||||
const { open } = useNotification();
|
const { open } = useNotification();
|
||||||
const [activeTools, setActiveTools] = useState<string[]>([]);
|
const [activeTools, setActiveTools] = useState<string[]>([]);
|
||||||
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
|
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
|
||||||
@@ -58,6 +106,7 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
|||||||
const currentTime = data?.currentTime;
|
const currentTime = data?.currentTime;
|
||||||
const selectedDate = data?.selectedDate;
|
const selectedDate = data?.selectedDate;
|
||||||
const schemeName = data?.schemeName;
|
const schemeName = data?.schemeName;
|
||||||
|
const networkName = project?.networkName || NETWORK_NAME;
|
||||||
const isCompareMode = data?.isCompareMode ?? false;
|
const isCompareMode = data?.isCompareMode ?? false;
|
||||||
const toggleCompareMode = data?.toggleCompareMode;
|
const toggleCompareMode = data?.toggleCompareMode;
|
||||||
const canToggleCompare = Boolean(
|
const canToggleCompare = Boolean(
|
||||||
@@ -325,6 +374,261 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
|||||||
const [computedProperties, setComputedProperties] = useState<
|
const [computedProperties, setComputedProperties] = useState<
|
||||||
Record<string, any>
|
Record<string, any>
|
||||||
>({});
|
>({});
|
||||||
|
const [valveStatus, setValveStatus] = useState<LinkStatus | null>(null);
|
||||||
|
const [isValveStatusLoading, setIsValveStatusLoading] = useState(false);
|
||||||
|
const [isValveStatusSaving, setIsValveStatusSaving] = useState(false);
|
||||||
|
const [valveProperties, setValveProperties] = useState<ValveProperties>({
|
||||||
|
vType: null,
|
||||||
|
setting: null,
|
||||||
|
});
|
||||||
|
const [isValvePropertiesLoading, setIsValvePropertiesLoading] =
|
||||||
|
useState(false);
|
||||||
|
const [isValveSettingSaving, setIsValveSettingSaving] = useState(false);
|
||||||
|
|
||||||
|
const selectedFeature = highlightFeatures[0];
|
||||||
|
const selectedFeatureLayer = selectedFeature
|
||||||
|
?.getId()
|
||||||
|
?.toString()
|
||||||
|
.split(".")[0];
|
||||||
|
const selectedFeatureId = selectedFeature?.getProperties?.().id;
|
||||||
|
const selectedFeatureValveType = selectedFeature?.getProperties?.().v_type;
|
||||||
|
const selectedValveType =
|
||||||
|
valveProperties.vType ??
|
||||||
|
(selectedFeatureValveType ? String(selectedFeatureValveType) : null);
|
||||||
|
const selectedValveId =
|
||||||
|
showPropertyPanel && isValveLayer(selectedFeatureLayer) && selectedFeatureId
|
||||||
|
? String(selectedFeatureId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedValveId) {
|
||||||
|
setValveStatus(null);
|
||||||
|
setIsValveStatusLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const queryValveStatus = async () => {
|
||||||
|
setIsValveStatusLoading(true);
|
||||||
|
setValveStatus(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
network: networkName,
|
||||||
|
link: selectedValveId,
|
||||||
|
});
|
||||||
|
const response = await apiFetch(
|
||||||
|
`${config.BACKEND_URL}/api/v1/getstatus/?${params.toString()}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`getstatus failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!cancelled) {
|
||||||
|
setValveStatus(isLinkStatus(data?.status) ? data.status : null);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error querying valve status:", error);
|
||||||
|
if (!cancelled) {
|
||||||
|
setValveStatus(null);
|
||||||
|
open?.({
|
||||||
|
type: "error",
|
||||||
|
message: "读取阀门开关状态失败。",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) {
|
||||||
|
setIsValveStatusLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
queryValveStatus();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [networkName, open, selectedValveId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedValveId) {
|
||||||
|
setValveProperties({ vType: null, setting: null });
|
||||||
|
setIsValvePropertiesLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const queryValveProperties = async () => {
|
||||||
|
setIsValvePropertiesLoading(true);
|
||||||
|
setValveProperties({ vType: null, setting: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
network: networkName,
|
||||||
|
valve: selectedValveId,
|
||||||
|
});
|
||||||
|
const response = await apiFetch(
|
||||||
|
`${config.BACKEND_URL}/api/v1/getvalveproperties/?${params.toString()}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`getvalveproperties failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!cancelled) {
|
||||||
|
setValveProperties({
|
||||||
|
vType: data?.v_type ? String(data.v_type) : null,
|
||||||
|
setting: normalizeValveSetting(data?.setting),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error querying valve properties:", error);
|
||||||
|
if (!cancelled) {
|
||||||
|
setValveProperties({ vType: null, setting: null });
|
||||||
|
open?.({
|
||||||
|
type: "error",
|
||||||
|
message: "读取阀门设置值失败。",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) {
|
||||||
|
setIsValvePropertiesLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
queryValveProperties();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [networkName, open, selectedValveId]);
|
||||||
|
|
||||||
|
const handleValveStatusSave = useCallback(
|
||||||
|
async (nextStatus: string) => {
|
||||||
|
if (!selectedValveId || !isLinkStatus(nextStatus)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsValveStatusSaving(true);
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
network: networkName,
|
||||||
|
link: selectedValveId,
|
||||||
|
});
|
||||||
|
const response = await apiFetch(
|
||||||
|
`${config.BACKEND_URL}/api/v1/setstatus/?${params.toString()}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ status: nextStatus }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`setstatus failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setValveStatus(nextStatus);
|
||||||
|
open?.({
|
||||||
|
type: "success",
|
||||||
|
message: "阀门开关状态已更新。",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating valve status:", error);
|
||||||
|
open?.({
|
||||||
|
type: "error",
|
||||||
|
message: "阀门开关状态更新失败。",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsValveStatusSaving(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[networkName, open, selectedValveId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleValveSettingSave = useCallback(
|
||||||
|
async (nextSetting: string) => {
|
||||||
|
if (!selectedValveId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valveStatus === "OPEN" || valveStatus === "CLOSED") {
|
||||||
|
open?.({
|
||||||
|
type: "error",
|
||||||
|
message: "开启或关闭状态下不能编辑阀门设置值。",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationMessage = validateValveSetting(
|
||||||
|
selectedValveType,
|
||||||
|
nextSetting,
|
||||||
|
);
|
||||||
|
if (validationMessage) {
|
||||||
|
open?.({
|
||||||
|
type: "error",
|
||||||
|
message: validationMessage,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedSetting = nextSetting.trim();
|
||||||
|
setIsValveSettingSaving(true);
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
network: networkName,
|
||||||
|
valve: selectedValveId,
|
||||||
|
});
|
||||||
|
const response = await apiFetch(
|
||||||
|
`${config.BACKEND_URL}/api/v1/setvalveproperties/?${params.toString()}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ setting: trimmedSetting }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`setvalveproperties failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setValveProperties((prev) => ({
|
||||||
|
...prev,
|
||||||
|
setting: trimmedSetting,
|
||||||
|
}));
|
||||||
|
open?.({
|
||||||
|
type: "success",
|
||||||
|
message: "阀门设置值已更新。",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating valve setting:", error);
|
||||||
|
open?.({
|
||||||
|
type: "error",
|
||||||
|
message: "阀门设置值更新失败。",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsValveSettingSaving(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
networkName,
|
||||||
|
open,
|
||||||
|
selectedValveId,
|
||||||
|
selectedValveType,
|
||||||
|
valveStatus,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// 添加 useEffect 来查询计算属性
|
// 添加 useEffect 来查询计算属性
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (highlightFeatures.length === 0 || !selectedDate || !showPropertyPanel) {
|
if (highlightFeatures.length === 0 || !selectedDate || !showPropertyPanel) {
|
||||||
@@ -387,8 +691,43 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
|||||||
}, [highlightFeatures, currentTime, selectedDate, queryType, schemeName, schemeType, showPropertyPanel]);
|
}, [highlightFeatures, currentTime, selectedDate, queryType, schemeName, schemeType, showPropertyPanel]);
|
||||||
|
|
||||||
const propertyPanelData = useMemo(
|
const propertyPanelData = useMemo(
|
||||||
() => buildFeatureProperties(highlightFeatures[0], computedProperties),
|
() =>
|
||||||
[highlightFeatures, computedProperties],
|
buildFeatureProperties(
|
||||||
|
selectedFeature,
|
||||||
|
computedProperties,
|
||||||
|
selectedValveId
|
||||||
|
? {
|
||||||
|
value: valveStatus,
|
||||||
|
loading: isValveStatusLoading,
|
||||||
|
saving: isValveStatusSaving,
|
||||||
|
onSave: handleValveStatusSave,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
selectedValveId
|
||||||
|
? {
|
||||||
|
value: valveProperties.setting,
|
||||||
|
vType: selectedValveType,
|
||||||
|
loading: isValvePropertiesLoading,
|
||||||
|
saving: isValveSettingSaving,
|
||||||
|
status: valveStatus,
|
||||||
|
onSave: handleValveSettingSave,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
),
|
||||||
|
[
|
||||||
|
selectedFeature,
|
||||||
|
computedProperties,
|
||||||
|
selectedValveId,
|
||||||
|
valveStatus,
|
||||||
|
valveProperties,
|
||||||
|
selectedValveType,
|
||||||
|
isValveStatusLoading,
|
||||||
|
isValveStatusSaving,
|
||||||
|
isValvePropertiesLoading,
|
||||||
|
isValveSettingSaving,
|
||||||
|
handleValveStatusSave,
|
||||||
|
handleValveSettingSave,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|||||||
@@ -16,7 +16,33 @@ type ToolbarTableProperty = {
|
|||||||
rows: (string | number)[][];
|
rows: (string | number)[][];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ToolbarPropertyItem = ToolbarBaseProperty | ToolbarTableProperty;
|
type ToolbarSelectProperty = {
|
||||||
|
type: "select";
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
options: { label: string; value: string }[];
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
saving?: boolean;
|
||||||
|
onSave: (value: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ToolbarTextProperty = {
|
||||||
|
type: "text";
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
saving?: boolean;
|
||||||
|
helperText?: string;
|
||||||
|
onSave: (value: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ToolbarPropertyItem =
|
||||||
|
| ToolbarBaseProperty
|
||||||
|
| ToolbarTableProperty
|
||||||
|
| ToolbarSelectProperty
|
||||||
|
| ToolbarTextProperty;
|
||||||
|
|
||||||
export type ToolbarPropertyPanelData = {
|
export type ToolbarPropertyPanelData = {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -24,6 +50,46 @@ export type ToolbarPropertyPanelData = {
|
|||||||
properties?: ToolbarPropertyItem[];
|
properties?: ToolbarPropertyItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ValveStatusPropertyOptions = {
|
||||||
|
value: string | null;
|
||||||
|
loading?: boolean;
|
||||||
|
saving?: boolean;
|
||||||
|
onSave: (value: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ValveSettingPropertyOptions = {
|
||||||
|
value: string | null;
|
||||||
|
vType: string | null;
|
||||||
|
loading?: boolean;
|
||||||
|
saving?: boolean;
|
||||||
|
status?: string | null;
|
||||||
|
onSave: (value: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getValveSettingHelperText = (
|
||||||
|
vType: string | null,
|
||||||
|
status?: string | null,
|
||||||
|
) => {
|
||||||
|
if (status === "OPEN" || status === "CLOSED") {
|
||||||
|
return "开启/关闭状态下 EPANET 会忽略阀门设置值";
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (vType?.toUpperCase()) {
|
||||||
|
case "PRV":
|
||||||
|
case "PSV":
|
||||||
|
case "PBV":
|
||||||
|
return "压力设置值,需为 0 或正数";
|
||||||
|
case "FCV":
|
||||||
|
return "流量设置值,需为 0 或正数";
|
||||||
|
case "TCV":
|
||||||
|
return "损失系数,需为 0 或正数";
|
||||||
|
case "GPV":
|
||||||
|
return "水头损失曲线 ID";
|
||||||
|
default:
|
||||||
|
return "阀门类型相关设置值";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getFeatureHistoryType = (feature: Feature): string | null => {
|
const getFeatureHistoryType = (feature: Feature): string | null => {
|
||||||
const layerId = feature.getId()?.toString().split(".")[0] || "";
|
const layerId = feature.getId()?.toString().split(".")[0] || "";
|
||||||
if (layerId.includes("pipe")) return "pipe";
|
if (layerId.includes("pipe")) return "pipe";
|
||||||
@@ -56,6 +122,8 @@ export const inferHistoryFeatureInfos = (
|
|||||||
export const buildFeatureProperties = (
|
export const buildFeatureProperties = (
|
||||||
highlightFeature: Feature | undefined,
|
highlightFeature: Feature | undefined,
|
||||||
computedProperties: Record<string, any>,
|
computedProperties: Record<string, any>,
|
||||||
|
valveStatus?: ValveStatusPropertyOptions,
|
||||||
|
valveSetting?: ValveSettingPropertyOptions,
|
||||||
): ToolbarPropertyPanelData => {
|
): ToolbarPropertyPanelData => {
|
||||||
if (!highlightFeature) return {};
|
if (!highlightFeature) return {};
|
||||||
|
|
||||||
@@ -267,6 +335,12 @@ export const buildFeatureProperties = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (layer === "geo_valves_mat" || layer === "geo_valves") {
|
if (layer === "geo_valves_mat" || layer === "geo_valves") {
|
||||||
|
const valveType = valveSetting?.vType ?? properties.v_type;
|
||||||
|
const isValveSettingDisabled =
|
||||||
|
valveSetting?.loading ||
|
||||||
|
valveSetting?.status === "OPEN" ||
|
||||||
|
valveSetting?.status === "CLOSED";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: properties.id,
|
id: properties.id,
|
||||||
type: "阀门",
|
type: "阀门",
|
||||||
@@ -280,12 +354,47 @@ export const buildFeatureProperties = (
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "阀门类型",
|
label: "阀门类型",
|
||||||
value: properties.v_type,
|
value: valveType,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "局部损失",
|
label: "局部损失",
|
||||||
value: properties.minor_loss?.toFixed?.(2),
|
value: properties.minor_loss?.toFixed?.(2),
|
||||||
},
|
},
|
||||||
|
...(valveStatus
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: "select" as const,
|
||||||
|
label: "开关状态",
|
||||||
|
value: valveStatus.value ?? "",
|
||||||
|
options: [
|
||||||
|
{ label: "开启", value: "OPEN" },
|
||||||
|
{ label: "关闭", value: "CLOSED" },
|
||||||
|
{ label: "激活", value: "ACTIVE" },
|
||||||
|
],
|
||||||
|
placeholder: valveStatus.loading ? "加载中" : "未设置",
|
||||||
|
disabled: valveStatus.loading,
|
||||||
|
saving: valveStatus.saving,
|
||||||
|
onSave: valveStatus.onSave,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(valveSetting
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
label: "阀门设置值",
|
||||||
|
value: valveSetting.value ?? "",
|
||||||
|
placeholder: valveSetting.loading ? "加载中" : "未设置",
|
||||||
|
disabled: isValveSettingDisabled,
|
||||||
|
saving: valveSetting.saving,
|
||||||
|
helperText: getValveSettingHelperText(
|
||||||
|
valveType,
|
||||||
|
valveSetting.status,
|
||||||
|
),
|
||||||
|
onSave: valveSetting.onSave,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user