feat(ui): 统一面板空状态引导
This commit is contained in:
@@ -23,6 +23,7 @@ import { bbox, featureCollection } from "@turf/turf";
|
||||
import { useMap } from "@components/olmap/core/MapComponent";
|
||||
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
||||
import { BurstDetectionResult, BurstDetectionRow } from "./types";
|
||||
import PanelEmptyState from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
export interface BurstDetectionResultsState {
|
||||
selectedDay: number | null;
|
||||
@@ -73,17 +74,11 @@ const formatDateTime = (value?: string) =>
|
||||
value ? dayjs(value).format("YYYY-MM-DD HH:mm") : "-";
|
||||
|
||||
const EmptyState = () => (
|
||||
<Box className="flex h-full flex-col items-center justify-center bg-gray-50/50 p-6 text-center">
|
||||
<Box className="mb-4 rounded-full bg-white p-6 shadow-sm">
|
||||
<ShowChartIcon sx={{ fontSize: 48, color: "#cbd5e1" }} />
|
||||
</Box>
|
||||
<Typography variant="h6" className="mb-1 font-bold text-gray-700">
|
||||
等待侦测结果
|
||||
</Typography>
|
||||
<Typography variant="body2" className="max-w-xs text-gray-500">
|
||||
提交侦测后,这里会展示目标时刻状态、前 14 天参考分数和异常测点。
|
||||
</Typography>
|
||||
</Box>
|
||||
<PanelEmptyState
|
||||
icon={<ShowChartIcon />}
|
||||
title="尚未生成侦测结果"
|
||||
description="请在“侦测参数”中运行分析,或在“方案查询”中打开历史结果。"
|
||||
/>
|
||||
);
|
||||
|
||||
const DetectionResults: React.FC<Props> = ({ result, state, onStateChange }) => {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useNotification } from "@refinedev/core";
|
||||
import { api } from "@/lib/api";
|
||||
import { NETWORK_NAME } from "@config/config";
|
||||
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||
import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState";
|
||||
import {
|
||||
BurstDetectionResult,
|
||||
BurstDetectionSchemeDetail,
|
||||
@@ -42,6 +43,7 @@ export interface BurstDetectionSchemeQueryState {
|
||||
queryAll: boolean;
|
||||
queryDate: Dayjs | null;
|
||||
expandedId: number | null;
|
||||
hasQueried: boolean;
|
||||
}
|
||||
|
||||
export const createBurstDetectionSchemeQueryState =
|
||||
@@ -49,6 +51,7 @@ export const createBurstDetectionSchemeQueryState =
|
||||
queryAll: true,
|
||||
queryDate: dayjs(),
|
||||
expandedId: null,
|
||||
hasQueried: false,
|
||||
});
|
||||
|
||||
const SchemeQuery: React.FC<Props> = ({
|
||||
@@ -64,7 +67,7 @@ const SchemeQuery: React.FC<Props> = ({
|
||||
onStateChange,
|
||||
createBurstDetectionSchemeQueryState(),
|
||||
);
|
||||
const { queryAll, queryDate, expandedId } = queryState;
|
||||
const { queryAll, queryDate, expandedId, hasQueried } = queryState;
|
||||
const [internalSchemes, setInternalSchemes] = useState<BurstDetectionSchemeRecord[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes;
|
||||
@@ -138,6 +141,7 @@ const SchemeQuery: React.FC<Props> = ({
|
||||
const response = await api.get("/api/v1/schemes", { params });
|
||||
const nextSchemes = response.data as BurstDetectionSchemeRecord[];
|
||||
setSchemes(nextSchemes);
|
||||
setQueryField("hasQueried", true);
|
||||
open?.({
|
||||
type: "success",
|
||||
message: "查询成功",
|
||||
@@ -203,7 +207,10 @@ const SchemeQuery: React.FC<Props> = ({
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={queryAll}
|
||||
onChange={(event) => setQueryField("queryAll", event.target.checked)}
|
||||
onChange={(event) => {
|
||||
setQueryField("queryAll", event.target.checked);
|
||||
setQueryField("hasQueried", false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2">查询全部</Typography>}
|
||||
@@ -212,7 +219,10 @@ const SchemeQuery: React.FC<Props> = ({
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
|
||||
<DatePicker
|
||||
value={queryDate}
|
||||
onChange={(value) => setQueryField("queryDate", value)}
|
||||
onChange={(value) => {
|
||||
setQueryField("queryDate", value);
|
||||
setQueryField("hasQueried", false);
|
||||
}}
|
||||
disabled={queryAll}
|
||||
format="YYYY-MM-DD"
|
||||
slotProps={{ textField: { size: "small", sx: { width: 180 } } }}
|
||||
@@ -234,12 +244,7 @@ const SchemeQuery: React.FC<Props> = ({
|
||||
|
||||
<Box className="flex-1 overflow-auto">
|
||||
{sortedSchemes.length === 0 ? (
|
||||
<Box className="flex h-full flex-col items-center justify-center text-center text-gray-400">
|
||||
<Typography variant="body2">暂无侦测方案</Typography>
|
||||
<Typography variant="caption" className="mt-1">
|
||||
运行一次展示版侦测后,可在这里回看历史结果。
|
||||
</Typography>
|
||||
</Box>
|
||||
<SchemeQueryEmptyState hasQueried={hasQueried} />
|
||||
) : (
|
||||
<Box className="space-y-2 p-2">
|
||||
<Typography variant="caption" className="px-2 text-gray-500">
|
||||
|
||||
@@ -31,6 +31,7 @@ import { Stroke, Style, Circle, Fill } from "ol/style";
|
||||
import { bbox, featureCollection } from "@turf/turf";
|
||||
import { BurstCandidate, BurstLocationResult } from "./types";
|
||||
import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units";
|
||||
import PanelEmptyState from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
interface Props {
|
||||
result: BurstLocationResult | null;
|
||||
@@ -128,17 +129,11 @@ const MetricCard = ({ label, value, hint, tone }: MetricCardProps) => {
|
||||
};
|
||||
|
||||
const EmptyState = () => (
|
||||
<Box className="flex h-full flex-col items-center justify-center bg-gray-50/50 p-6 text-center">
|
||||
<Box className="mb-4 rounded-full bg-white p-6 shadow-sm">
|
||||
<MapIcon sx={{ fontSize: 48, color: "#cbd5e1" }} />
|
||||
</Box>
|
||||
<Typography variant="h6" className="mb-1 font-bold text-gray-700">
|
||||
等待定位结果
|
||||
</Typography>
|
||||
<Typography variant="body2" className="max-w-xs text-gray-500">
|
||||
请先提交爆管定位分析,结果面板将展示定位摘要、时间窗、采样情况和候选管段。
|
||||
</Typography>
|
||||
</Box>
|
||||
<PanelEmptyState
|
||||
icon={<MapIcon />}
|
||||
title="尚未生成定位结果"
|
||||
description="请在“定位参数”中运行分析,或在“方案查询”中打开历史结果。"
|
||||
/>
|
||||
);
|
||||
|
||||
const LocationResults: React.FC<Props> = ({ result }) => {
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
} from "./types";
|
||||
import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units";
|
||||
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
|
||||
import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
interface Props {
|
||||
onViewResult: (result: BurstLocationResult) => void;
|
||||
@@ -57,6 +58,7 @@ export interface BurstLocationSchemeQueryState {
|
||||
queryDate: Dayjs | null;
|
||||
expandedId: number | null;
|
||||
simulationBurstIdsByName: Record<string, string[]>;
|
||||
hasQueried: boolean;
|
||||
}
|
||||
|
||||
export const createBurstLocationSchemeQueryState =
|
||||
@@ -65,6 +67,7 @@ export const createBurstLocationSchemeQueryState =
|
||||
queryDate: dayjs(),
|
||||
expandedId: null,
|
||||
simulationBurstIdsByName: {},
|
||||
hasQueried: false,
|
||||
});
|
||||
|
||||
const SchemeQuery: React.FC<Props> = ({
|
||||
@@ -83,7 +86,7 @@ const SchemeQuery: React.FC<Props> = ({
|
||||
onStateChange,
|
||||
createBurstLocationSchemeQueryState(),
|
||||
);
|
||||
const { queryAll, queryDate, expandedId } = queryState;
|
||||
const { queryAll, queryDate, expandedId, hasQueried } = queryState;
|
||||
const simulationBurstIdsByName = queryState.simulationBurstIdsByName ?? {};
|
||||
const [internalSchemes, setInternalSchemes] = useState<BurstSchemeRecord[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -256,6 +259,7 @@ const SchemeQuery: React.FC<Props> = ({
|
||||
),
|
||||
),
|
||||
);
|
||||
setQueryField("hasQueried", true);
|
||||
open?.({
|
||||
type: "success",
|
||||
message: "查询成功",
|
||||
@@ -320,7 +324,10 @@ const SchemeQuery: React.FC<Props> = ({
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={queryAll}
|
||||
onChange={(e) => setQueryField("queryAll", e.target.checked)}
|
||||
onChange={(e) => {
|
||||
setQueryField("queryAll", e.target.checked);
|
||||
setQueryField("hasQueried", false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2">查询全部</Typography>}
|
||||
@@ -329,7 +336,10 @@ const SchemeQuery: React.FC<Props> = ({
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
|
||||
<DatePicker
|
||||
value={queryDate}
|
||||
onChange={(value) => setQueryField("queryDate", value)}
|
||||
onChange={(value) => {
|
||||
setQueryField("queryDate", value);
|
||||
setQueryField("hasQueried", false);
|
||||
}}
|
||||
disabled={queryAll}
|
||||
format="YYYY-MM-DD"
|
||||
slotProps={{ textField: { size: "small", sx: { width: 200 } } }}
|
||||
@@ -350,39 +360,7 @@ const SchemeQuery: React.FC<Props> = ({
|
||||
</Box>
|
||||
<Box className="flex-1 overflow-auto">
|
||||
{sortedSchemes.length === 0 ? (
|
||||
<Box className="flex flex-col items-center justify-center h-full text-gray-400">
|
||||
<Box className="mb-4">
|
||||
<svg
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 80 80"
|
||||
fill="none"
|
||||
className="opacity-40"
|
||||
>
|
||||
<rect
|
||||
x="10"
|
||||
y="20"
|
||||
width="60"
|
||||
height="45"
|
||||
rx="2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1="10"
|
||||
y1="30"
|
||||
x2="70"
|
||||
y2="30"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
<Typography variant="body2">总共 0 条</Typography>
|
||||
<Typography variant="body2" className="mt-1">
|
||||
No data
|
||||
</Typography>
|
||||
</Box>
|
||||
<SchemeQueryEmptyState hasQueried={hasQueried} />
|
||||
) : (
|
||||
<Box className="space-y-2 p-2">
|
||||
<Typography variant="caption" className="text-gray-500 px-2">
|
||||
|
||||
@@ -62,6 +62,24 @@ describe("AnalysisReport", () => {
|
||||
document.body.classList.remove("burst-analysis-report-printing");
|
||||
});
|
||||
|
||||
it("guides the user to choose a scheme when no report is active", () => {
|
||||
render(
|
||||
<AnalysisReport
|
||||
scheme={null}
|
||||
valveResult={null}
|
||||
disabledValves={[]}
|
||||
generatedAt={null}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("尚未选择分析方案")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"请在“方案查询”中打开一个方案,再查看分析报告。",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the selected scheme, pipe data, and matching valve result", async () => {
|
||||
render(
|
||||
<AnalysisReport
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
type PipeDiameterMap,
|
||||
} from "./schemePipeDiameters";
|
||||
import { SchemeRecord, ValveIsolationResult } from "./types";
|
||||
import PanelEmptyState from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
interface AnalysisReportProps {
|
||||
scheme: SchemeRecord | null;
|
||||
@@ -516,15 +517,11 @@ const AnalysisReport: React.FC<AnalysisReportProps> = ({
|
||||
|
||||
if (!scheme) {
|
||||
return (
|
||||
<Box className="flex h-full flex-col items-center justify-center px-6 text-center">
|
||||
<DescriptionOutlined sx={{ mb: 2, fontSize: 52, color: "#94a3b8" }} />
|
||||
<Typography variant="h6" className="font-bold text-gray-700">
|
||||
等待选择分析方案
|
||||
</Typography>
|
||||
<Typography variant="body2" className="mt-2 text-gray-500">
|
||||
请在“方案查询”中点击“查看分析报告”。
|
||||
</Typography>
|
||||
</Box>
|
||||
<PanelEmptyState
|
||||
icon={<DescriptionOutlined />}
|
||||
title="尚未选择分析方案"
|
||||
description="请在“方案查询”中打开一个方案,再查看分析报告。"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
type PipeDiameterMap,
|
||||
} from "./schemePipeDiameters";
|
||||
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
|
||||
import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
interface SchemeQueryProps {
|
||||
schemes?: SchemeRecord[];
|
||||
@@ -76,6 +77,7 @@ export interface BurstSchemeQueryState {
|
||||
selectedDate: Date | undefined;
|
||||
timeRange: { start: Date; end: Date } | undefined;
|
||||
expandedId: number | null;
|
||||
hasQueried: boolean;
|
||||
}
|
||||
|
||||
export const createBurstSchemeQueryState = (): BurstSchemeQueryState => ({
|
||||
@@ -85,6 +87,7 @@ export const createBurstSchemeQueryState = (): BurstSchemeQueryState => ({
|
||||
selectedDate: undefined,
|
||||
timeRange: undefined,
|
||||
expandedId: null,
|
||||
hasQueried: false,
|
||||
});
|
||||
|
||||
const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
@@ -107,6 +110,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
selectedDate,
|
||||
timeRange,
|
||||
expandedId,
|
||||
hasQueried,
|
||||
} = queryState;
|
||||
const [highlightLayer, setHighlightLayer] =
|
||||
useState<VectorLayer<VectorSource> | null>(null);
|
||||
@@ -185,6 +189,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
schemeDetail: item.scheme_detail,
|
||||
}));
|
||||
setSchemes(nextSchemes);
|
||||
setQueryField("hasQueried", true);
|
||||
|
||||
if (filteredResults.length === 0) {
|
||||
open?.({
|
||||
@@ -462,7 +467,10 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
control={
|
||||
<Checkbox
|
||||
checked={queryAll}
|
||||
onChange={(e) => setQueryField("queryAll", e.target.checked)}
|
||||
onChange={(e) => {
|
||||
setQueryField("queryAll", e.target.checked);
|
||||
setQueryField("hasQueried", false);
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
@@ -475,9 +483,12 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
>
|
||||
<DatePicker
|
||||
value={queryDate}
|
||||
onChange={(value) =>
|
||||
value && dayjs.isDayjs(value) && setQueryField("queryDate", value)
|
||||
}
|
||||
onChange={(value) => {
|
||||
if (value && dayjs.isDayjs(value)) {
|
||||
setQueryField("queryDate", value);
|
||||
setQueryField("hasQueried", false);
|
||||
}
|
||||
}}
|
||||
format="YYYY-MM-DD"
|
||||
disabled={queryAll}
|
||||
slotProps={{
|
||||
@@ -505,39 +516,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
{/* 结果列表 */}
|
||||
<Box className="flex-1 overflow-auto">
|
||||
{filteredSchemes.length === 0 ? (
|
||||
<Box className="flex flex-col items-center justify-center h-full text-gray-400">
|
||||
<Box className="mb-4">
|
||||
<svg
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 80 80"
|
||||
fill="none"
|
||||
className="opacity-40"
|
||||
>
|
||||
<rect
|
||||
x="10"
|
||||
y="20"
|
||||
width="60"
|
||||
height="45"
|
||||
rx="2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1="10"
|
||||
y1="30"
|
||||
x2="70"
|
||||
y2="30"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
<Typography variant="body2">总共 0 条</Typography>
|
||||
<Typography variant="body2" className="mt-1">
|
||||
No data
|
||||
</Typography>
|
||||
</Box>
|
||||
<SchemeQueryEmptyState hasQueried={hasQueried} />
|
||||
) : (
|
||||
<Box className="space-y-2 p-2">
|
||||
<Typography variant="caption" className="text-gray-500 px-2">
|
||||
|
||||
@@ -41,6 +41,7 @@ import { bbox, featureCollection } from "@turf/turf";
|
||||
import Timeline from "@components/olmap/core/Controls/Timeline";
|
||||
import { ContaminantSchemaItem, ContaminantSchemeRecord } from "./types";
|
||||
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
|
||||
import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
interface SchemeQueryProps {
|
||||
schemes?: ContaminantSchemeRecord[];
|
||||
@@ -60,6 +61,7 @@ export interface ContaminantSchemeQueryState {
|
||||
selectedDate: Date | undefined;
|
||||
timeRange: { start: Date; end: Date } | undefined;
|
||||
expandedId: number | null;
|
||||
hasQueried: boolean;
|
||||
}
|
||||
|
||||
export const createContaminantSchemeQueryState =
|
||||
@@ -70,6 +72,7 @@ export const createContaminantSchemeQueryState =
|
||||
selectedDate: undefined,
|
||||
timeRange: undefined,
|
||||
expandedId: null,
|
||||
hasQueried: false,
|
||||
});
|
||||
|
||||
const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
@@ -92,6 +95,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
selectedDate,
|
||||
timeRange,
|
||||
expandedId,
|
||||
hasQueried,
|
||||
} = queryState;
|
||||
const [highlightLayer, setHighlightLayer] =
|
||||
useState<VectorLayer<VectorSource> | null>(null);
|
||||
@@ -250,6 +254,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
schemeDetail: item.scheme_detail,
|
||||
}));
|
||||
setSchemes(nextSchemes);
|
||||
setQueryField("hasQueried", true);
|
||||
|
||||
if (filteredResults.length === 0) {
|
||||
open?.({
|
||||
@@ -351,7 +356,10 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
control={
|
||||
<Checkbox
|
||||
checked={queryAll}
|
||||
onChange={(e) => setQueryField("queryAll", e.target.checked)}
|
||||
onChange={(e) => {
|
||||
setQueryField("queryAll", e.target.checked);
|
||||
setQueryField("hasQueried", false);
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
@@ -364,9 +372,12 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
>
|
||||
<DatePicker
|
||||
value={queryDate}
|
||||
onChange={(value) =>
|
||||
value && dayjs.isDayjs(value) && setQueryField("queryDate", value)
|
||||
}
|
||||
onChange={(value) => {
|
||||
if (value && dayjs.isDayjs(value)) {
|
||||
setQueryField("queryDate", value);
|
||||
setQueryField("hasQueried", false);
|
||||
}
|
||||
}}
|
||||
format="YYYY-MM-DD"
|
||||
disabled={queryAll}
|
||||
slotProps={{
|
||||
@@ -393,39 +404,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
|
||||
<Box className="flex-1 overflow-auto">
|
||||
{filteredSchemes.length === 0 ? (
|
||||
<Box className="flex flex-col items-center justify-center h-full text-gray-400">
|
||||
<Box className="mb-4">
|
||||
<svg
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 80 80"
|
||||
fill="none"
|
||||
className="opacity-40"
|
||||
>
|
||||
<rect
|
||||
x="10"
|
||||
y="20"
|
||||
width="60"
|
||||
height="45"
|
||||
rx="2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1="10"
|
||||
y1="30"
|
||||
x2="70"
|
||||
y2="30"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
<Typography variant="body2">总共 0 条</Typography>
|
||||
<Typography variant="body2" className="mt-1">
|
||||
No data
|
||||
</Typography>
|
||||
</Box>
|
||||
<SchemeQueryEmptyState hasQueried={hasQueried} />
|
||||
) : (
|
||||
<Box className="space-y-2 p-2">
|
||||
<Typography variant="caption" className="text-gray-500 px-2">
|
||||
|
||||
@@ -11,11 +11,15 @@ import {
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@mui/material";
|
||||
import { FormatListBulleted } from "@mui/icons-material";
|
||||
import {
|
||||
FormatListBulleted,
|
||||
TroubleshootOutlined,
|
||||
} from "@mui/icons-material";
|
||||
import dayjs from "dayjs";
|
||||
import { getAreaColor } from "./utils";
|
||||
import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units";
|
||||
import { LeakageResultDetail } from "./types";
|
||||
import PanelEmptyState from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
interface Props {
|
||||
result: LeakageResultDetail | null;
|
||||
@@ -31,39 +35,11 @@ const RecognitionResults: React.FC<Props> = ({ result }) => {
|
||||
|
||||
if (!result || !sortedRows.length) {
|
||||
return (
|
||||
<Box className="flex flex-col items-center justify-center h-full text-gray-400 p-4">
|
||||
<Box className="mb-4">
|
||||
<svg
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 80 80"
|
||||
fill="none"
|
||||
className="opacity-40"
|
||||
>
|
||||
<rect
|
||||
x="10"
|
||||
y="20"
|
||||
width="60"
|
||||
height="45"
|
||||
rx="2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1="10"
|
||||
y1="30"
|
||||
x2="70"
|
||||
y2="30"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
<Typography variant="body2">暂无识别结果</Typography>
|
||||
<Typography variant="body2" className="mt-1">
|
||||
请先加载方案或执行识别分析
|
||||
</Typography>
|
||||
</Box>
|
||||
<PanelEmptyState
|
||||
icon={<TroubleshootOutlined />}
|
||||
title="尚未生成识别结果"
|
||||
description="请在“识别参数”中运行分析,或在“方案查询”中打开历史结果。"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import { useControllableObjectState } from "@components/olmap/core/useControllab
|
||||
import { LeakageResultDetail, LeakageSchemeRecord } from "./types";
|
||||
import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units";
|
||||
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
|
||||
import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
interface Props {
|
||||
onViewResult: (result: LeakageResultDetail) => void;
|
||||
@@ -40,12 +41,14 @@ export interface DMALeakSchemeQueryState {
|
||||
queryAll: boolean;
|
||||
queryDate: Dayjs | null;
|
||||
expandedId: number | null;
|
||||
hasQueried: boolean;
|
||||
}
|
||||
|
||||
export const createDMALeakSchemeQueryState = (): DMALeakSchemeQueryState => ({
|
||||
queryAll: true,
|
||||
queryDate: dayjs(),
|
||||
expandedId: null,
|
||||
hasQueried: false,
|
||||
});
|
||||
|
||||
const SchemeQuery: React.FC<Props> = ({
|
||||
@@ -61,7 +64,7 @@ const SchemeQuery: React.FC<Props> = ({
|
||||
onStateChange,
|
||||
createDMALeakSchemeQueryState(),
|
||||
);
|
||||
const { queryAll, queryDate, expandedId } = queryState;
|
||||
const { queryAll, queryDate, expandedId, hasQueried } = queryState;
|
||||
const [internalSchemes, setInternalSchemes] = useState<LeakageSchemeRecord[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const creatorName = useSchemeCreatorName();
|
||||
@@ -93,6 +96,7 @@ const SchemeQuery: React.FC<Props> = ({
|
||||
});
|
||||
const nextSchemes = response.data as LeakageSchemeRecord[];
|
||||
setSchemes(nextSchemes);
|
||||
setQueryField("hasQueried", true);
|
||||
if (nextSchemes.length === 0) {
|
||||
open?.({
|
||||
type: "success",
|
||||
@@ -150,7 +154,10 @@ const SchemeQuery: React.FC<Props> = ({
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={queryAll}
|
||||
onChange={(e) => setQueryField("queryAll", e.target.checked)}
|
||||
onChange={(e) => {
|
||||
setQueryField("queryAll", e.target.checked);
|
||||
setQueryField("hasQueried", false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2">查询全部</Typography>}
|
||||
@@ -159,7 +166,10 @@ const SchemeQuery: React.FC<Props> = ({
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
|
||||
<DatePicker
|
||||
value={queryDate}
|
||||
onChange={(value) => setQueryField("queryDate", value)}
|
||||
onChange={(value) => {
|
||||
setQueryField("queryDate", value);
|
||||
setQueryField("hasQueried", false);
|
||||
}}
|
||||
disabled={queryAll}
|
||||
format="YYYY-MM-DD"
|
||||
slotProps={{ textField: { size: "small", sx: { width: 200 } } }}
|
||||
@@ -180,39 +190,7 @@ const SchemeQuery: React.FC<Props> = ({
|
||||
</Box>
|
||||
<Box className="flex-1 overflow-auto">
|
||||
{sortedSchemes.length === 0 ? (
|
||||
<Box className="flex flex-col items-center justify-center h-full text-gray-400">
|
||||
<Box className="mb-4">
|
||||
<svg
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 80 80"
|
||||
fill="none"
|
||||
className="opacity-40"
|
||||
>
|
||||
<rect
|
||||
x="10"
|
||||
y="20"
|
||||
width="60"
|
||||
height="45"
|
||||
rx="2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1="10"
|
||||
y1="30"
|
||||
x2="70"
|
||||
y2="30"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
<Typography variant="body2">总共 0 条</Typography>
|
||||
<Typography variant="body2" className="mt-1">
|
||||
No data
|
||||
</Typography>
|
||||
</Box>
|
||||
<SchemeQueryEmptyState hasQueried={hasQueried} />
|
||||
) : (
|
||||
<Box className="space-y-2 p-2">
|
||||
<Typography variant="caption" className="text-gray-500 px-2">
|
||||
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
Alert,
|
||||
Divider,
|
||||
} from "@mui/material";
|
||||
import { Close as CloseIcon } from "@mui/icons-material";
|
||||
import {
|
||||
AdjustOutlined,
|
||||
Close as CloseIcon,
|
||||
WaterDropOutlined,
|
||||
} from "@mui/icons-material";
|
||||
import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||
import { zhCN as pickerZhCN } from "@mui/x-date-pickers/locales";
|
||||
@@ -31,6 +35,7 @@ import { useNotification } from "@refinedev/core";
|
||||
import { api } from "@/lib/api";
|
||||
import { config, NETWORK_NAME } from "@/config/config";
|
||||
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||
import PanelEmptyState from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
export interface ValveItem {
|
||||
id: string;
|
||||
@@ -383,9 +388,12 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
|
||||
</Box>
|
||||
))}
|
||||
{valves.length === 0 && (
|
||||
<Typography variant="caption" className="text-gray-400 text-center py-20">
|
||||
暂无选中阀门
|
||||
</Typography>
|
||||
<PanelEmptyState
|
||||
variant="compact"
|
||||
icon={<AdjustOutlined />}
|
||||
title="尚未选择阀门"
|
||||
description="点击“选择阀门”,然后在地图上添加。"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
@@ -412,7 +420,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
|
||||
💡 点击地图上的节点作为排水点
|
||||
</Box>
|
||||
)}
|
||||
<Stack spacing={1} className="h-12 overflow-auto">
|
||||
<Stack spacing={1} className="min-h-16 overflow-auto">
|
||||
{drainageNode && (
|
||||
<Box className="flex items-center gap-2 p-2 bg-gray-50 rounded">
|
||||
<Typography className="text-sm flex-1 pl-1">{drainageNode}</Typography>
|
||||
@@ -428,9 +436,12 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
|
||||
</Box>
|
||||
)}
|
||||
{!drainageNode && (
|
||||
<Typography variant="caption" className="text-gray-400 text-center py-2">
|
||||
暂无选中排水节点
|
||||
</Typography>
|
||||
<PanelEmptyState
|
||||
variant="compact"
|
||||
icon={<WaterDropOutlined />}
|
||||
title="尚未选择排水节点"
|
||||
description="点击“选择节点”,然后在地图上指定排水点。"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
@@ -43,6 +43,7 @@ import Timeline from "@components/olmap/core/Controls/Timeline";
|
||||
import { SchemeRecord, SchemaItem } from "./types";
|
||||
import { FLOW_DISPLAY_UNIT } from "@utils/units";
|
||||
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
|
||||
import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
interface SchemeQueryProps {
|
||||
schemes?: SchemeRecord[];
|
||||
@@ -61,6 +62,7 @@ export interface FlushingSchemeQueryState {
|
||||
showTimeline: boolean;
|
||||
selectedDate: Date | undefined;
|
||||
timeRange: { start: Date; end: Date } | undefined;
|
||||
hasQueried: boolean;
|
||||
}
|
||||
|
||||
export const createFlushingSchemeQueryState =
|
||||
@@ -71,6 +73,7 @@ export const createFlushingSchemeQueryState =
|
||||
showTimeline: false,
|
||||
selectedDate: undefined,
|
||||
timeRange: undefined,
|
||||
hasQueried: false,
|
||||
});
|
||||
|
||||
const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
@@ -92,6 +95,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
showTimeline,
|
||||
selectedDate,
|
||||
timeRange,
|
||||
hasQueried,
|
||||
} = queryState;
|
||||
const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
@@ -298,6 +302,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
schemeDetail: item.scheme_detail,
|
||||
}));
|
||||
setSchemes(nextSchemes);
|
||||
setQueryField("hasQueried", true);
|
||||
|
||||
if (filteredResults.length === 0) {
|
||||
open?.({
|
||||
@@ -371,7 +376,10 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
control={
|
||||
<Checkbox
|
||||
checked={queryAll}
|
||||
onChange={(e) => setQueryField("queryAll", e.target.checked)}
|
||||
onChange={(e) => {
|
||||
setQueryField("queryAll", e.target.checked);
|
||||
setQueryField("hasQueried", false);
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
@@ -384,9 +392,12 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
>
|
||||
<DatePicker
|
||||
value={queryDate}
|
||||
onChange={(value) =>
|
||||
value && dayjs.isDayjs(value) && setQueryField("queryDate", value)
|
||||
}
|
||||
onChange={(value) => {
|
||||
if (value && dayjs.isDayjs(value)) {
|
||||
setQueryField("queryDate", value);
|
||||
setQueryField("hasQueried", false);
|
||||
}
|
||||
}}
|
||||
format="YYYY-MM-DD"
|
||||
disabled={queryAll}
|
||||
slotProps={{
|
||||
@@ -414,39 +425,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
{/* Results List */}
|
||||
<Box className="flex-1 overflow-auto">
|
||||
{sortedSchemes.length === 0 ? (
|
||||
<Box className="flex flex-col items-center justify-center h-full text-gray-400">
|
||||
<Box className="mb-4">
|
||||
<svg
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 80 80"
|
||||
fill="none"
|
||||
className="opacity-40"
|
||||
>
|
||||
<rect
|
||||
x="10"
|
||||
y="20"
|
||||
width="60"
|
||||
height="45"
|
||||
rx="2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1="10"
|
||||
y1="30"
|
||||
x2="70"
|
||||
y2="30"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
<Typography variant="body2">总共 0 条</Typography>
|
||||
<Typography variant="body2" className="mt-1">
|
||||
No data
|
||||
</Typography>
|
||||
</Box>
|
||||
<SchemeQueryEmptyState hasQueried={hasQueried} />
|
||||
) : (
|
||||
<Box className="space-y-2 p-2">
|
||||
<Typography variant="caption" className="text-gray-500 px-2">
|
||||
|
||||
@@ -23,6 +23,7 @@ import { RAINBOW_COLORS, RISK_BREAKS, RISK_LABELS } from "./types";
|
||||
import { useHealthRisk } from "./HealthRiskContext";
|
||||
import { useProject } from "@/contexts/ProjectContext";
|
||||
import HealthRiskReport from "./HealthRiskReport";
|
||||
import PanelEmptyState from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
const SIMPLE_LABELS = [
|
||||
"0.0 - 0.1",
|
||||
@@ -229,24 +230,11 @@ const HealthRiskStatistics: React.FC = () => {
|
||||
};
|
||||
|
||||
const renderEmpty = () => (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
color: "text.secondary",
|
||||
}}
|
||||
>
|
||||
<BarChart sx={{ fontSize: 64, mb: 2, opacity: 0.3 }} />
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 500 }}>
|
||||
暂无预测数据
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
请先进行健康风险分析预测,以生成风险分布数据
|
||||
</Typography>
|
||||
</Box>
|
||||
<PanelEmptyState
|
||||
icon={<BarChart />}
|
||||
title="暂无风险预测结果"
|
||||
description="请先运行健康风险预测,完成后将在这里展示风险分布统计。"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ShowChart } from "@mui/icons-material";
|
||||
import ReactECharts from "echarts-for-react";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import { useHealthRisk } from "./HealthRiskContext";
|
||||
import PanelEmptyState from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
export interface PredictDataPanelProps {
|
||||
/** 选中的要素信息列表,格式为 [[id, type], [id, type]] */
|
||||
@@ -58,26 +59,11 @@ const PredictDataPanel: React.FC<PredictDataPanelProps> = ({
|
||||
}, [filteredResults]);
|
||||
|
||||
const renderEmpty = () => (
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
py: 8,
|
||||
color: "text.secondary",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<ShowChart sx={{ fontSize: 64, mb: 2, opacity: 0.3 }} />
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 500 }}>
|
||||
暂无预测数据
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
请在地图上选择已分析的管道
|
||||
</Typography>
|
||||
</Box>
|
||||
<PanelEmptyState
|
||||
icon={<ShowChart />}
|
||||
title="暂无可展示的管道预测"
|
||||
description="请先在地图上选择已完成健康风险分析的管道。"
|
||||
/>
|
||||
);
|
||||
|
||||
const renderChart = () => {
|
||||
|
||||
@@ -38,6 +38,7 @@ import { Style, Icon, Circle, Fill, Stroke } from "ol/style";
|
||||
import Feature, { FeatureLike } from "ol/Feature";
|
||||
import { bbox, featureCollection } from "@turf/turf";
|
||||
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
|
||||
import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
interface SchemeRecord {
|
||||
id: number;
|
||||
@@ -72,6 +73,7 @@ export interface MonitoringSchemeQueryState {
|
||||
queryAll: boolean;
|
||||
queryDate: Dayjs | null;
|
||||
expandedId: number | null;
|
||||
hasQueried: boolean;
|
||||
}
|
||||
|
||||
export const createMonitoringSchemeQueryState =
|
||||
@@ -79,6 +81,7 @@ export const createMonitoringSchemeQueryState =
|
||||
queryAll: true,
|
||||
queryDate: dayjs(new Date()),
|
||||
expandedId: null,
|
||||
hasQueried: false,
|
||||
});
|
||||
|
||||
const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
@@ -94,7 +97,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
onStateChange,
|
||||
createMonitoringSchemeQueryState(),
|
||||
);
|
||||
const { queryAll, queryDate, expandedId } = queryState;
|
||||
const { queryAll, queryDate, expandedId, hasQueried } = queryState;
|
||||
const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const creatorName = useSchemeCreatorName();
|
||||
@@ -207,6 +210,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
sensorLocation: item.sensor_location,
|
||||
}));
|
||||
setSchemes(nextSchemes);
|
||||
setQueryField("hasQueried", true);
|
||||
|
||||
if (filteredResults.length === 0) {
|
||||
open?.({
|
||||
@@ -290,7 +294,10 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
control={
|
||||
<Checkbox
|
||||
checked={queryAll}
|
||||
onChange={(e) => setQueryField("queryAll", e.target.checked)}
|
||||
onChange={(e) => {
|
||||
setQueryField("queryAll", e.target.checked);
|
||||
setQueryField("hasQueried", false);
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
@@ -303,9 +310,12 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
>
|
||||
<DatePicker
|
||||
value={queryDate}
|
||||
onChange={(value) =>
|
||||
value && dayjs.isDayjs(value) && setQueryField("queryDate", value)
|
||||
}
|
||||
onChange={(value) => {
|
||||
if (value && dayjs.isDayjs(value)) {
|
||||
setQueryField("queryDate", value);
|
||||
setQueryField("hasQueried", false);
|
||||
}
|
||||
}}
|
||||
format="YYYY-MM-DD"
|
||||
disabled={queryAll}
|
||||
slotProps={{
|
||||
@@ -333,39 +343,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
{/* 结果列表 */}
|
||||
<Box className="flex-1 overflow-auto">
|
||||
{sortedSchemes.length === 0 ? (
|
||||
<Box className="flex flex-col items-center justify-center h-full text-gray-400">
|
||||
<Box className="mb-4">
|
||||
<svg
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 80 80"
|
||||
fill="none"
|
||||
className="opacity-40"
|
||||
>
|
||||
<rect
|
||||
x="10"
|
||||
y="20"
|
||||
width="60"
|
||||
height="45"
|
||||
rx="2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1="10"
|
||||
y1="30"
|
||||
x2="70"
|
||||
y2="30"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
<Typography variant="body2">总共 0 条</Typography>
|
||||
<Typography variant="body2" className="mt-1">
|
||||
No data
|
||||
</Typography>
|
||||
</Box>
|
||||
<SchemeQueryEmptyState hasQueried={hasQueried} />
|
||||
) : (
|
||||
<Box className="space-y-2 p-2">
|
||||
<Typography variant="caption" className="text-gray-500 px-2">
|
||||
|
||||
@@ -40,6 +40,7 @@ import { useGetIdentity } from "@refinedev/core";
|
||||
import { useNotification } from "@refinedev/core";
|
||||
import { api } from "@/lib/api";
|
||||
import { apiFetch } from "@/lib/apiFetch";
|
||||
import PanelEmptyState from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
@@ -314,11 +315,11 @@ const emptyStateMessages: Record<
|
||||
> = {
|
||||
chart: {
|
||||
title: "暂无时序数据",
|
||||
subtitle: "请切换时间段来获取曲线",
|
||||
subtitle: "请调整时间范围,或确认所选设备在该时段已有数据。",
|
||||
},
|
||||
table: {
|
||||
title: "暂无表格数据",
|
||||
subtitle: "请切换时间段来获取记录",
|
||||
subtitle: "请调整时间范围,或确认所选设备在该时段已有数据。",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -721,28 +722,23 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
||||
);
|
||||
|
||||
const renderEmpty = () => {
|
||||
if (!hasDevices) {
|
||||
return (
|
||||
<PanelEmptyState
|
||||
icon={<ShowChart />}
|
||||
title="尚未选择设备"
|
||||
description="请先在设备列表中选择需要查看的 SCADA 设备。"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const message = emptyStateMessages[activeTab];
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
py: 8,
|
||||
color: "text.secondary",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<ShowChart sx={{ fontSize: 64, mb: 2, opacity: 0.3 }} />
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 500 }}>
|
||||
{message.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{message.subtitle}
|
||||
</Typography>
|
||||
</Box>
|
||||
<PanelEmptyState
|
||||
icon={activeTab === "chart" ? <ShowChart /> : <TableChart />}
|
||||
title={message.title}
|
||||
description={message.subtitle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1254,15 +1250,6 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
||||
</Stack>
|
||||
</LocalizationProvider>
|
||||
|
||||
{!hasDevices && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="warning.main"
|
||||
sx={{ mt: 1, display: "block" }}
|
||||
>
|
||||
未选择任何设备,无法获取数据。
|
||||
</Typography>
|
||||
)}
|
||||
{error && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
|
||||
@@ -68,6 +68,7 @@ import dayjs, { Dayjs } from "dayjs";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers";
|
||||
import { zhCN as pickerZhCN } from "@mui/x-date-pickers/locales";
|
||||
import PanelEmptyState from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
const STATUS_OPTIONS: {
|
||||
value: "online" | "offline" | "warning" | "error";
|
||||
@@ -1067,22 +1068,23 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : filteredDevices.length === 0 ? (
|
||||
<Box
|
||||
sx={{
|
||||
p: 4,
|
||||
textAlign: "center",
|
||||
color: "text.secondary",
|
||||
}}
|
||||
>
|
||||
<DeviceHub sx={{ fontSize: 48, mb: 2, opacity: 0.5 }} />
|
||||
<Typography variant="body2">
|
||||
{searchQuery ||
|
||||
<PanelEmptyState
|
||||
icon={<DeviceHub />}
|
||||
title={
|
||||
searchQuery ||
|
||||
selectedType !== "all" ||
|
||||
selectedStatus !== "all"
|
||||
? "未找到匹配的设备"
|
||||
: "暂无 SCADA 设备"}
|
||||
</Typography>
|
||||
</Box>
|
||||
? "未找到匹配设备"
|
||||
: "暂无 SCADA 设备"
|
||||
}
|
||||
description={
|
||||
searchQuery ||
|
||||
selectedType !== "all" ||
|
||||
selectedStatus !== "all"
|
||||
? "请清除关键词,或调整设备类型和状态筛选条件。"
|
||||
: "请确认设备已接入当前管网后刷新列表。"
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<FixedSizeList
|
||||
height={listHeight}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { DescriptionOutlined } from "@mui/icons-material";
|
||||
import PanelEmptyState, { SchemeQueryEmptyState } from "./PanelEmptyState";
|
||||
|
||||
describe("PanelEmptyState", () => {
|
||||
it("renders an accessible panel instruction", () => {
|
||||
render(
|
||||
<PanelEmptyState
|
||||
icon={<DescriptionOutlined />}
|
||||
title="尚未选择分析方案"
|
||||
description="请在方案查询中打开一个方案。"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("status")).toHaveAttribute("aria-live", "polite");
|
||||
expect(screen.getByText("尚未选择分析方案")).toBeInTheDocument();
|
||||
expect(screen.getByText("请在方案查询中打开一个方案。")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("distinguishes initial query guidance from an empty result", () => {
|
||||
const { rerender } = render(
|
||||
<SchemeQueryEmptyState hasQueried={false} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText("尚未查询历史方案")).toBeInTheDocument();
|
||||
|
||||
rerender(<SchemeQueryEmptyState hasQueried />);
|
||||
|
||||
expect(screen.getByText("未找到符合条件的方案")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("请调整查询条件后重新查询,或先创建一个新方案。"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("supports compact selection guidance", () => {
|
||||
render(
|
||||
<PanelEmptyState
|
||||
variant="compact"
|
||||
title="尚未选择阀门"
|
||||
description="点击“选择阀门”,然后在地图上添加。"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("尚未选择阀门")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { SearchOffOutlined } from "@mui/icons-material";
|
||||
import { alpha } from "@mui/material/styles";
|
||||
|
||||
export interface PanelEmptyStateProps {
|
||||
icon?: ReactNode;
|
||||
title: string;
|
||||
description?: string;
|
||||
variant?: "panel" | "compact";
|
||||
}
|
||||
|
||||
const PanelEmptyState = ({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
variant = "panel",
|
||||
}: PanelEmptyStateProps) => {
|
||||
const compact = variant === "compact";
|
||||
|
||||
return (
|
||||
<Box
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
lang="zh-CN"
|
||||
sx={(theme) => ({
|
||||
width: "100%",
|
||||
height: compact ? "auto" : "100%",
|
||||
minHeight: compact ? 48 : 220,
|
||||
display: "flex",
|
||||
flexDirection: compact ? "row" : { xs: "column", sm: "row" },
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: compact ? 1.5 : { xs: 1.75, sm: 2.5 },
|
||||
px: compact ? 1.5 : { xs: 2, sm: 4 },
|
||||
py: compact ? 1.25 : { xs: 4, sm: 5 },
|
||||
color: "text.secondary",
|
||||
textAlign: compact ? "left" : { xs: "center", sm: "left" },
|
||||
boxSizing: "border-box",
|
||||
"& .MuiTypography-root": {
|
||||
textWrap: "pretty",
|
||||
},
|
||||
"& .MuiSvgIcon-root": {
|
||||
fontSize: compact ? 22 : 32,
|
||||
},
|
||||
"& > [aria-hidden='true']": {
|
||||
flex: "0 0 auto",
|
||||
width: compact ? 36 : 56,
|
||||
height: compact ? 36 : 56,
|
||||
borderRadius: compact ? 2 : 3,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
color: "primary.main",
|
||||
backgroundColor: alpha(theme.palette.primary.main, 0.08),
|
||||
boxShadow: `inset 0 0 0 1px ${alpha(
|
||||
theme.palette.primary.main,
|
||||
0.12,
|
||||
)}`,
|
||||
},
|
||||
})}
|
||||
>
|
||||
{icon ? <Box aria-hidden="true">{icon}</Box> : null}
|
||||
<Box sx={{ minWidth: 0, maxWidth: compact ? "none" : 380 }}>
|
||||
<Typography
|
||||
variant={compact ? "body2" : "subtitle1"}
|
||||
sx={{
|
||||
color: "text.primary",
|
||||
fontWeight: 600,
|
||||
lineHeight: compact ? 1.5 : 1.55,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{description ? (
|
||||
<Typography
|
||||
variant={compact ? "caption" : "body2"}
|
||||
sx={{
|
||||
display: "block",
|
||||
mt: compact ? 0.25 : 0.75,
|
||||
color: "text.secondary",
|
||||
lineHeight: compact ? 1.6 : 1.7,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export interface SchemeQueryEmptyStateProps {
|
||||
hasQueried: boolean;
|
||||
}
|
||||
|
||||
export const SchemeQueryEmptyState = ({
|
||||
hasQueried,
|
||||
}: SchemeQueryEmptyStateProps) => (
|
||||
<PanelEmptyState
|
||||
icon={<SearchOffOutlined />}
|
||||
title={hasQueried ? "未找到符合条件的方案" : "尚未查询历史方案"}
|
||||
description={
|
||||
hasQueried
|
||||
? "请调整查询条件后重新查询,或先创建一个新方案。"
|
||||
: "设置查询条件后点击“查询”,这里将显示可查看的历史方案。"
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export default PanelEmptyState;
|
||||
@@ -36,6 +36,7 @@ import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers";
|
||||
import { zhCN as pickerZhCN } from "@mui/x-date-pickers/locales";
|
||||
import config from "@/config/config";
|
||||
import { apiFetch } from "@/lib/apiFetch";
|
||||
import PanelEmptyState from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
@@ -413,11 +414,11 @@ const emptyStateMessages: Record<
|
||||
> = {
|
||||
chart: {
|
||||
title: "暂无时序数据",
|
||||
subtitle: "请切换时间段来获取曲线",
|
||||
subtitle: "请调整时间范围,或确认所选设备在该时段已有数据。",
|
||||
},
|
||||
table: {
|
||||
title: "暂无表格数据",
|
||||
subtitle: "请切换时间段来获取记录",
|
||||
subtitle: "请调整时间范围,或确认所选设备在该时段已有数据。",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -613,28 +614,23 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
||||
);
|
||||
|
||||
const renderEmpty = () => {
|
||||
if (!hasDevices) {
|
||||
return (
|
||||
<PanelEmptyState
|
||||
icon={<ShowChart />}
|
||||
title="尚未选择地图要素"
|
||||
description="请在地图上选择一个要素以查看历史数据。"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const message = emptyStateMessages[activeTab];
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
py: 8,
|
||||
color: "text.secondary",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<ShowChart sx={{ fontSize: 64, mb: 2, opacity: 0.3 }} />
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 500 }}>
|
||||
{message.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{message.subtitle}
|
||||
</Typography>
|
||||
</Box>
|
||||
<PanelEmptyState
|
||||
icon={activeTab === "chart" ? <ShowChart /> : <TableChart />}
|
||||
title={message.title}
|
||||
description={message.subtitle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1048,15 +1044,6 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
||||
</Stack>
|
||||
</LocalizationProvider>
|
||||
|
||||
{!hasDevices && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="warning.main"
|
||||
sx={{ mt: 1, display: "block" }}
|
||||
>
|
||||
请选择一个要素以查看其历史数据。
|
||||
</Typography>
|
||||
)}
|
||||
{error && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import Draggable from "react-draggable";
|
||||
import { Close } from "@mui/icons-material";
|
||||
import { Close, InfoOutlined } from "@mui/icons-material";
|
||||
import {
|
||||
Button,
|
||||
CircularProgress,
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import type { SelectChangeEvent } from "@mui/material/Select";
|
||||
import PanelEmptyState from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
interface BaseProperty {
|
||||
label: string;
|
||||
@@ -162,23 +163,11 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||
{/* 内容区域 */}
|
||||
<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
|
||||
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>
|
||||
<PanelEmptyState
|
||||
icon={<InfoOutlined />}
|
||||
title="尚未选择地图要素"
|
||||
description="请在地图上选择管道、节点或设备以查看属性。"
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{/* ID 属性 */}
|
||||
|
||||
Reference in New Issue
Block a user