更新属性面板为可拖动,优化工具栏激活逻辑
This commit is contained in:
@@ -169,7 +169,7 @@ const App = (props: React.PropsWithChildren<AppProps>) => {
|
|||||||
{
|
{
|
||||||
name: "Hydraulic Simulation",
|
name: "Hydraulic Simulation",
|
||||||
meta: {
|
meta: {
|
||||||
icon: <MdWater className="w-6 h-6" />,
|
// icon: <MdWater className="w-6 h-6" />,
|
||||||
label: "事件模拟",
|
label: "事件模拟",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -986,7 +986,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
<Box
|
<Box
|
||||||
className="absolute top-20 right-4 bg-white shadow-2xl rounded-lg cursor-pointer hover:shadow-xl transition-all duration-300 opacity-95 hover:opacity-100"
|
className="absolute top-20 right-4 bg-white shadow-2xl rounded-lg cursor-pointer hover:shadow-xl transition-all duration-300 opacity-95 hover:opacity-100"
|
||||||
onClick={() => setIsExpanded(true)}
|
onClick={() => setIsExpanded(true)}
|
||||||
sx={{ zIndex: 1300 }}
|
sx={{ zIndex: 1290 }}
|
||||||
>
|
>
|
||||||
<Box className="flex flex-col items-center py-3 px-3 gap-1">
|
<Box className="flex flex-col items-center py-3 px-3 gap-1">
|
||||||
<ShowChart className="text-[#257DD4] w-5 h-5" />
|
<ShowChart className="text-[#257DD4] w-5 h-5" />
|
||||||
|
|||||||
@@ -129,14 +129,17 @@ const fetchFromBackend = async (
|
|||||||
"raw"
|
"raw"
|
||||||
);
|
);
|
||||||
} else if (type === "scheme") {
|
} else if (type === "scheme") {
|
||||||
// 查询策略模拟值、清洗值和监测值
|
// 查询策略模拟值、实时模拟值、清洗值和监测值
|
||||||
const [cleanedRes, rawRes, schemeSimRes] = await Promise.all([
|
const [cleanedRes, rawRes, simulationRes, schemeSimRes] = await Promise.all([
|
||||||
apiFetch(cleanedDataUrl)
|
apiFetch(cleanedDataUrl)
|
||||||
.then((r) => (r.ok ? r.json() : null))
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
.catch(() => null),
|
.catch(() => null),
|
||||||
apiFetch(rawDataUrl)
|
apiFetch(rawDataUrl)
|
||||||
.then((r) => (r.ok ? r.json() : null))
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
.catch(() => null),
|
.catch(() => null),
|
||||||
|
apiFetch(simulationDataUrl)
|
||||||
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
|
.catch(() => null),
|
||||||
apiFetch(schemeSimulationDataUrl)
|
apiFetch(schemeSimulationDataUrl)
|
||||||
.then((r) => (r.ok ? r.json() : null))
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
.catch(() => null),
|
.catch(() => null),
|
||||||
@@ -146,40 +149,18 @@ const fetchFromBackend = async (
|
|||||||
// 如果清洗数据有值,则不显示原始监测值
|
// 如果清洗数据有值,则不显示原始监测值
|
||||||
const rawData =
|
const rawData =
|
||||||
cleanedData.length > 0 ? [] : transformBackendData(rawRes, featureIds);
|
cleanedData.length > 0 ? [] : transformBackendData(rawRes, featureIds);
|
||||||
|
const simulationData = transformBackendData(simulationRes, featureIds);
|
||||||
const schemeSimData = transformBackendData(schemeSimRes, featureIds);
|
const schemeSimData = transformBackendData(schemeSimRes, featureIds);
|
||||||
|
|
||||||
// 合并三组数据
|
return mergeMultipleTimeSeriesData(
|
||||||
const timeMap = new Map<string, Record<string, number | null>>();
|
[
|
||||||
|
{ data: cleanedData, suffix: "clean" },
|
||||||
[cleanedData, rawData, schemeSimData].forEach((data, index) => {
|
{ data: rawData, suffix: "raw" },
|
||||||
const suffix = ["clean", "raw", "scheme_sim"][index];
|
{ data: simulationData, suffix: "sim" },
|
||||||
data.forEach((point) => {
|
{ data: schemeSimData, suffix: "scheme_sim" },
|
||||||
if (!timeMap.has(point.timestamp)) {
|
],
|
||||||
timeMap.set(point.timestamp, {});
|
featureIds
|
||||||
}
|
|
||||||
const values = timeMap.get(point.timestamp)!;
|
|
||||||
featureIds.forEach((deviceId) => {
|
|
||||||
const value = point.values[deviceId];
|
|
||||||
if (value !== undefined) {
|
|
||||||
values[`${deviceId}_${suffix}`] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = Array.from(timeMap.entries()).map(
|
|
||||||
([timestamp, values]) => ({
|
|
||||||
timestamp,
|
|
||||||
values,
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
result.sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
||||||
);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} else {
|
} else {
|
||||||
// realtime: 查询模拟值、清洗值和监测值
|
// realtime: 查询模拟值、清洗值和监测值
|
||||||
const [cleanedRes, rawRes, simulationRes] = await Promise.all([
|
const [cleanedRes, rawRes, simulationRes] = await Promise.all([
|
||||||
@@ -336,6 +317,42 @@ const mergeTimeSeriesData = (
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mergeMultipleTimeSeriesData = (
|
||||||
|
datasets: Array<{
|
||||||
|
data: TimeSeriesPoint[];
|
||||||
|
suffix: string;
|
||||||
|
}>,
|
||||||
|
deviceIds: string[]
|
||||||
|
): TimeSeriesPoint[] => {
|
||||||
|
const timeMap = new Map<string, Record<string, number | null>>();
|
||||||
|
|
||||||
|
datasets.forEach(({ data, suffix }) => {
|
||||||
|
data.forEach((point) => {
|
||||||
|
if (!timeMap.has(point.timestamp)) {
|
||||||
|
timeMap.set(point.timestamp, {});
|
||||||
|
}
|
||||||
|
const values = timeMap.get(point.timestamp)!;
|
||||||
|
deviceIds.forEach((deviceId) => {
|
||||||
|
const value = point.values[deviceId];
|
||||||
|
if (value !== undefined) {
|
||||||
|
values[`${deviceId}_${suffix}`] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = Array.from(timeMap.entries()).map(([timestamp, values]) => ({
|
||||||
|
timestamp,
|
||||||
|
values,
|
||||||
|
}));
|
||||||
|
|
||||||
|
result.sort(
|
||||||
|
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
const formatTimestamp = (timestamp: string) =>
|
const formatTimestamp = (timestamp: string) =>
|
||||||
dayjs(timestamp).tz("Asia/Shanghai").format("YYYY-MM-DD HH:mm");
|
dayjs(timestamp).tz("Asia/Shanghai").format("YYYY-MM-DD HH:mm");
|
||||||
|
|
||||||
@@ -537,7 +554,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
const suffixes = [
|
const suffixes = [
|
||||||
{ key: "clean", name: "清洗值" },
|
{ key: "clean", name: "清洗值" },
|
||||||
{ key: "raw", name: "监测值" },
|
{ key: "raw", name: "监测值" },
|
||||||
{ key: "sim", name: "模拟值" },
|
{ key: "sim", name: "实时模拟值" },
|
||||||
{ key: "scheme_sim", name: "方案模拟值" },
|
{ key: "scheme_sim", name: "方案模拟值" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -643,32 +660,46 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
: suffix === "raw"
|
: suffix === "raw"
|
||||||
? "监测值"
|
? "监测值"
|
||||||
: suffix === "sim"
|
: suffix === "sim"
|
||||||
? "模拟"
|
? "实时模拟"
|
||||||
: "方案模拟";
|
: "方案模拟";
|
||||||
|
|
||||||
series.push({
|
series.push({
|
||||||
name: `${id} (${displayName})`,
|
name: `${id} (${displayName})`,
|
||||||
type: "line",
|
type: "line",
|
||||||
symbol: "none",
|
symbol:
|
||||||
|
suffix === "clean"
|
||||||
|
? "circle"
|
||||||
|
: suffix === "raw"
|
||||||
|
? "diamond"
|
||||||
|
: "none",
|
||||||
|
symbolSize: suffix === "clean" || suffix === "raw" ? 7 : 0,
|
||||||
|
showSymbol: suffix === "clean" || suffix === "raw",
|
||||||
sampling: "lttb",
|
sampling: "lttb",
|
||||||
connectNulls: true,
|
connectNulls: suffix !== "clean" && suffix !== "raw",
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: colors[(index * 4 + sIndex) % colors.length],
|
color: colors[(index * 4 + sIndex) % colors.length],
|
||||||
},
|
},
|
||||||
data: dataset.map((item) => item[key]),
|
data: dataset.map((item) => item[key]),
|
||||||
areaStyle: {
|
lineStyle:
|
||||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
suffix === "clean" || suffix === "raw"
|
||||||
{
|
? { width: 0 }
|
||||||
offset: 0,
|
: undefined,
|
||||||
color: colors[(index * 4 + sIndex) % colors.length],
|
areaStyle:
|
||||||
},
|
suffix === "clean" || suffix === "raw"
|
||||||
{
|
? undefined
|
||||||
offset: 1,
|
: {
|
||||||
color: "rgba(255, 255, 255, 0)",
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
},
|
{
|
||||||
]),
|
offset: 0,
|
||||||
opacity: 0.3,
|
color: colors[(index * 4 + sIndex) % colors.length],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: "rgba(255, 255, 255, 0)",
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
opacity: 0.3,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -840,7 +871,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
border: "none",
|
border: "none",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
zIndex: 1300,
|
zIndex: 1290,
|
||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
|
|
||||||
|
import React, { useRef } from "react";
|
||||||
|
import Draggable from "react-draggable";
|
||||||
|
|
||||||
interface BaseProperty {
|
interface BaseProperty {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -28,6 +31,8 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||||||
type = "未知类型",
|
type = "未知类型",
|
||||||
properties = [],
|
properties = [],
|
||||||
}) => {
|
}) => {
|
||||||
|
const draggableRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const formatValue = (property: BaseProperty) => {
|
const formatValue = (property: BaseProperty) => {
|
||||||
if (property.formatter) {
|
if (property.formatter) {
|
||||||
return property.formatter(property.value);
|
return property.formatter(property.value);
|
||||||
@@ -50,162 +55,16 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-4 right-4 bg-white shadow-2xl rounded-xl overflow-hidden w-96 max-h-[850px] flex flex-col backdrop-blur-sm z-1300 opacity-95 hover:opacity-100 transition-all duration-300 ">
|
<Draggable nodeRef={draggableRef} handle=".drag-handle">
|
||||||
{/* 头部 */}
|
<div
|
||||||
<div className="flex justify-between items-center px-5 py-4 bg-[#257DD4] text-white">
|
ref={draggableRef}
|
||||||
<div className="flex items-center gap-2">
|
className="absolute top-4 right-4 bg-white shadow-2xl rounded-xl overflow-hidden w-96 max-h-[850px] flex flex-col backdrop-blur-sm z-1300 opacity-95 hover:opacity-100"
|
||||||
<svg
|
>
|
||||||
className="w-5 h-5"
|
{/* 头部 */}
|
||||||
fill="none"
|
<div className="drag-handle flex justify-between items-center px-5 py-4 bg-[#257DD4] text-white cursor-move select-none">
|
||||||
stroke="currentColor"
|
<div className="flex items-center gap-2">
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<h3 className="text-lg font-semibold">属性面板</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 内容区域 */}
|
|
||||||
<div className="flex-1 overflow-y-auto px-4 py-3">
|
|
||||||
{!id ? (
|
|
||||||
<div className="flex flex-col items-center justify-center py-12 text-gray-400">
|
|
||||||
<svg
|
<svg
|
||||||
className="w-16 h-16 mb-3"
|
className="w-5 h-5"
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<p className="text-sm">暂无属性信息</p>
|
|
||||||
<p className="text-xs mt-1">请选择一个要素以查看其属性</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{/* ID 属性 */}
|
|
||||||
<div className="group rounded-lg p-3 transition-all duration-200 bg-blue-50 hover:bg-blue-100 border-l-4 border-blue-500">
|
|
||||||
<div className="flex justify-between items-start gap-3">
|
|
||||||
<span className="font-medium text-xs uppercase tracking-wide text-blue-700">
|
|
||||||
ID
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-semibold text-right flex-1 text-blue-900">
|
|
||||||
{id}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 类型属性 */}
|
|
||||||
<div className="group rounded-lg p-3 transition-all duration-200 bg-blue-50 hover:bg-blue-100 border-l-4 border-blue-500">
|
|
||||||
<div className="flex justify-between items-start gap-3">
|
|
||||||
<span className="font-medium text-xs uppercase tracking-wide text-blue-700">
|
|
||||||
类型
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-semibold text-right flex-1 text-blue-900">
|
|
||||||
{type}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 其他属性(包含二级表格) */}
|
|
||||||
{properties.map((property, index) => {
|
|
||||||
// 二级表格
|
|
||||||
if ("type" in property && property.type === "table") {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`table-${index}`}
|
|
||||||
className="group rounded-lg p-3 transition-all duration-200 bg-gray-50 hover:bg-gray-100"
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-start gap-3">
|
|
||||||
<span className="font-medium text-xs uppercase tracking-wide text-gray-600">
|
|
||||||
{property.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="ml-4 mt-2 border border-gray-300 rounded-md overflow-hidden shadow-sm">
|
|
||||||
<table className="w-full text-xs">
|
|
||||||
<thead className="bg-gray-200 text-gray-700">
|
|
||||||
<tr>
|
|
||||||
{property.columns.map((col, ci) => (
|
|
||||||
<th
|
|
||||||
key={ci}
|
|
||||||
className="px-3 py-2 text-left font-semibold"
|
|
||||||
>
|
|
||||||
{col}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-300">
|
|
||||||
{property.rows.map((row, ri) => (
|
|
||||||
<tr key={ri} className="bg-white hover:bg-gray-50">
|
|
||||||
{row.map((cell, cci) => (
|
|
||||||
<td
|
|
||||||
key={cci}
|
|
||||||
className="px-3 py-2 text-gray-800"
|
|
||||||
>
|
|
||||||
{cell}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 普通属性
|
|
||||||
const base = property as BaseProperty;
|
|
||||||
const isImportant = isImportantKeys.includes(base.label);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`prop-${index}`}
|
|
||||||
className={`group rounded-lg p-3 transition-all duration-200 ${
|
|
||||||
isImportant
|
|
||||||
? "bg-blue-50 hover:bg-blue-100 border-l-4 border-blue-500"
|
|
||||||
: "bg-gray-50 hover:bg-gray-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-start gap-3">
|
|
||||||
<span
|
|
||||||
className={`font-medium text-xs uppercase tracking-wide ${
|
|
||||||
isImportant ? "text-blue-700" : "text-gray-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{base.label}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={`text-sm font-semibold text-right flex-1 ${
|
|
||||||
isImportant ? "text-blue-900" : "text-gray-800"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{formatValue(base)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 底部统计区域 */}
|
|
||||||
<div className="px-5 py-3 bg-gray-50 border-t border-gray-200">
|
|
||||||
<div className="flex items-center justify-between text-xs">
|
|
||||||
<span className="text-gray-600 flex items-center gap-1">
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4"
|
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -214,20 +73,172 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
共 {totalProps} 个属性
|
<h3 className="text-lg font-semibold">属性面板</h3>
|
||||||
</span>
|
</div>
|
||||||
{id && (
|
</div>
|
||||||
<span className="text-green-600 flex items-center gap-1 font-medium">
|
|
||||||
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
|
{/* 内容区域 */}
|
||||||
已选中
|
<div className="flex-1 overflow-y-auto px-4 py-3">
|
||||||
</span>
|
{!id ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-gray-400">
|
||||||
|
<svg
|
||||||
|
className="w-16 h-16 mb-3"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p className="text-sm">暂无属性信息</p>
|
||||||
|
<p className="text-xs mt-1">请选择一个要素以查看其属性</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{/* ID 属性 */}
|
||||||
|
<div className="group rounded-lg p-3 transition-all duration-200 bg-blue-50 hover:bg-blue-100 border-l-4 border-blue-500">
|
||||||
|
<div className="flex justify-between items-start gap-3">
|
||||||
|
<span className="font-medium text-xs uppercase tracking-wide text-blue-700">
|
||||||
|
ID
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-semibold text-right flex-1 text-blue-900">
|
||||||
|
{id}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 类型属性 */}
|
||||||
|
<div className="group rounded-lg p-3 transition-all duration-200 bg-blue-50 hover:bg-blue-100 border-l-4 border-blue-500">
|
||||||
|
<div className="flex justify-between items-start gap-3">
|
||||||
|
<span className="font-medium text-xs uppercase tracking-wide text-blue-700">
|
||||||
|
类型
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-semibold text-right flex-1 text-blue-900">
|
||||||
|
{type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 其他属性(包含二级表格) */}
|
||||||
|
{properties.map((property, index) => {
|
||||||
|
// 二级表格
|
||||||
|
if ("type" in property && property.type === "table") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`table-${index}`}
|
||||||
|
className="group rounded-lg p-3 transition-all duration-200 bg-gray-50 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start gap-3">
|
||||||
|
<span className="font-medium text-xs uppercase tracking-wide text-gray-600">
|
||||||
|
{property.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 mt-2 border border-gray-300 rounded-md overflow-hidden shadow-sm">
|
||||||
|
<table className="w-full text-xs">
|
||||||
|
<thead className="bg-gray-200 text-gray-700">
|
||||||
|
<tr>
|
||||||
|
{property.columns.map((col, ci) => (
|
||||||
|
<th
|
||||||
|
key={ci}
|
||||||
|
className="px-3 py-2 text-left font-semibold"
|
||||||
|
>
|
||||||
|
{col}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-300">
|
||||||
|
{property.rows.map((row, ri) => (
|
||||||
|
<tr key={ri} className="bg-white hover:bg-gray-50">
|
||||||
|
{row.map((cell, cci) => (
|
||||||
|
<td
|
||||||
|
key={cci}
|
||||||
|
className="px-3 py-2 text-gray-800"
|
||||||
|
>
|
||||||
|
{cell}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通属性
|
||||||
|
const base = property as BaseProperty;
|
||||||
|
const isImportant = isImportantKeys.includes(base.label);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`prop-${index}`}
|
||||||
|
className={`group rounded-lg p-3 transition-all duration-200 ${
|
||||||
|
isImportant
|
||||||
|
? "bg-blue-50 hover:bg-blue-100 border-l-4 border-blue-500"
|
||||||
|
: "bg-gray-50 hover:bg-gray-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start gap-3">
|
||||||
|
<span
|
||||||
|
className={`font-medium text-xs uppercase tracking-wide ${
|
||||||
|
isImportant ? "text-blue-700" : "text-gray-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{base.label}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`text-sm font-semibold text-right flex-1 ${
|
||||||
|
isImportant ? "text-blue-900" : "text-gray-800"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{formatValue(base)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部统计区域 */}
|
||||||
|
<div className="px-5 py-3 bg-gray-50 border-t border-gray-200">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-gray-600 flex items-center gap-1">
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
共 {totalProps} 个属性
|
||||||
|
</span>
|
||||||
|
{id && (
|
||||||
|
<span className="text-green-600 flex items-center gap-1 font-medium">
|
||||||
|
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
|
||||||
|
已选中
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Draggable>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -402,15 +402,8 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
|||||||
deactivateTool(tool);
|
deactivateTool(tool);
|
||||||
setActiveTools((prev) => prev.filter((t) => t !== tool));
|
setActiveTools((prev) => prev.filter((t) => t !== tool));
|
||||||
} else {
|
} else {
|
||||||
// 如果当前工具未激活,先关闭所有其他工具,然后激活当前工具
|
// 如果当前工具未激活,保留其他已打开工具,仅新增当前工具
|
||||||
// 关闭所有面板(但保持样式编辑器状态)
|
setActiveTools((prev) => [...prev, tool]);
|
||||||
closeAllPanelsExceptStyle();
|
|
||||||
|
|
||||||
// 取消激活所有非样式工具
|
|
||||||
setActiveTools((prev) => {
|
|
||||||
const styleActive = prev.includes("style");
|
|
||||||
return styleActive ? ["style", tool] : [tool];
|
|
||||||
});
|
|
||||||
|
|
||||||
// 激活当前工具并打开对应面板
|
// 激活当前工具并打开对应面板
|
||||||
activateTool(tool);
|
activateTool(tool);
|
||||||
@@ -422,14 +415,18 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
|||||||
switch (tool) {
|
switch (tool) {
|
||||||
case "info":
|
case "info":
|
||||||
setShowPropertyPanel(false);
|
setShowPropertyPanel(false);
|
||||||
setHighlightFeatures([]);
|
if (!activeTools.includes("history")) {
|
||||||
|
setHighlightFeatures([]);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "draw":
|
case "draw":
|
||||||
setShowDrawPanel(false);
|
setShowDrawPanel(false);
|
||||||
break;
|
break;
|
||||||
case "history":
|
case "history":
|
||||||
setShowHistoryPanel(false);
|
setShowHistoryPanel(false);
|
||||||
setHighlightFeatures([]);
|
if (!activeTools.includes("info")) {
|
||||||
|
setHighlightFeatures([]);
|
||||||
|
}
|
||||||
setChatPanelFeatureInfos(null);
|
setChatPanelFeatureInfos(null);
|
||||||
setChatPanelTimeRange(null);
|
setChatPanelTimeRange(null);
|
||||||
break;
|
break;
|
||||||
@@ -452,16 +449,6 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 关闭所有面板(除了样式编辑器)
|
|
||||||
const closeAllPanelsExceptStyle = () => {
|
|
||||||
setShowPropertyPanel(false);
|
|
||||||
setHighlightFeatures([]);
|
|
||||||
setShowDrawPanel(false);
|
|
||||||
setShowHistoryPanel(false);
|
|
||||||
setChatPanelFeatureInfos(null);
|
|
||||||
setChatPanelTimeRange(null);
|
|
||||||
// 样式编辑器保持其当前状态,不自动关闭
|
|
||||||
};
|
|
||||||
const [computedProperties, setComputedProperties] = useState<
|
const [computedProperties, setComputedProperties] = useState<
|
||||||
Record<string, any>
|
Record<string, any>
|
||||||
>({});
|
>({});
|
||||||
|
|||||||
Reference in New Issue
Block a user