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