This commit is contained in:
2026-07-13 14:31:49 +08:00
parent dac7457c60
commit 06fd99fcf9
12 changed files with 666 additions and 186 deletions
+80 -23
View File
@@ -17,6 +17,7 @@ let currentNames: string[] = [];
let visibleNames: Record<string, boolean> = {};
let lastCursorTime: number | null = null;
let cursorFrame: number | undefined;
let draggingCursor = false;
const gridTop = 58;
const gridBottom = 52;
@@ -47,6 +48,14 @@ function timestamp(value: any) {
function displayValue(value: any) {
return typeof value === "number" ? Number(value.toFixed(3)) : value;
}
function displayDateTime(value: any) {
const time = timestamp(value);
if (!Number.isFinite(time)) return "—";
return new Date(time).toLocaleString("zh-CN", {
hour12: false,
timeZone: "Asia/Shanghai",
});
}
function axisLabel(value: number) {
const date = new Date(value);
const parts = new Intl.DateTimeFormat("zh-CN", {
@@ -186,9 +195,15 @@ function cursorRows(target: number) {
})
.filter(Boolean);
}
function clampCursorTime(target: number) {
return Math.min(Math.max(target, props.startTime), props.endTime);
}
function midpointCursorTime() {
return props.startTime + (props.endTime - props.startTime) / 2;
}
function emitCursorAt(target: number) {
if (!Number.isFinite(target)) return;
lastCursorTime = Math.min(Math.max(target, props.startTime), props.endTime);
lastCursorTime = clampCursorTime(target);
scheduleVisualCursor(lastCursorTime);
emit("cursor", cursorRows(lastCursorTime));
}
@@ -232,7 +247,7 @@ function updateVisualCursor(target: number) {
{ lazyUpdate: true },
);
}
function handleMouseMove(event: any) {
function pointerTargetTime(event: any) {
if (!chart) return;
const x = event.zrX ?? event.offsetX;
const y = event.zrY ?? event.offsetY;
@@ -242,7 +257,35 @@ function handleMouseMove(event: any) {
y,
]) as number[];
const target = timestamp(converted?.[0]);
if (Number.isFinite(target)) emitCursorAt(target);
return Number.isFinite(target) ? target : undefined;
}
function updateCursorFromPointer(event: any) {
const target = pointerTargetTime(event);
if (target !== undefined) emitCursorAt(target);
}
function isPrimaryMouseDown(event: any) {
const native = event.event;
return native?.button === undefined || native.button === 0;
}
function isPrimaryButtonStillPressed(event: any) {
const native = event.event;
return native?.buttons === undefined || (native.buttons & 1) === 1;
}
function handleMouseDown(event: any) {
if (!isPrimaryMouseDown(event)) return;
draggingCursor = true;
updateCursorFromPointer(event);
}
function handleMouseMove(event: any) {
if (!draggingCursor) return;
if (!isPrimaryButtonStillPressed(event)) {
draggingCursor = false;
return;
}
updateCursorFromPointer(event);
}
function stopCursorDrag() {
draggingCursor = false;
}
function handleLegendChange(event: any) {
visibleNames = { ...(event.selected ?? {}) };
@@ -281,6 +324,7 @@ function render() {
s.unit,
s.point_name,
s.point_id,
d.quality_reason,
];
if (d.quality === "bad") {
solid.push([
@@ -292,6 +336,7 @@ function render() {
s.unit,
s.point_name,
s.point_id,
d.quality_reason,
]);
bad.push([
d.ts,
@@ -302,6 +347,7 @@ function render() {
s.unit,
s.point_name,
s.point_id,
d.quality_reason,
]);
} else {
solid.push(row);
@@ -314,6 +360,7 @@ function render() {
s.unit,
s.point_name,
s.point_id,
d.quality_reason,
]);
}
});
@@ -323,11 +370,12 @@ function render() {
id: `${s.point_id}-good`,
name: currentNames[i],
type: "line",
showSymbol: false,
showSymbol: true,
symbolSize: 4,
connectNulls: false,
data: solid,
lineStyle: { width: 1.8, color },
itemStyle: { color },
itemStyle: { color, opacity: 0.82 },
emphasis: { focus: "series" },
},
{
@@ -368,27 +416,22 @@ function render() {
itemHeight: 8,
},
tooltip: {
trigger: "axis",
trigger: "item",
transitionDuration: 0.12,
axisPointer: {
type: "none",
},
backgroundColor: "#07141af2",
borderColor: "#35505a",
textStyle: { color: "#dce9e5" },
formatter: (params: any) => {
const rows = Array.isArray(params) ? params : [params];
const axisValue = rows[0]?.axisValue;
const target =
lastCursorTime ?? timestamp(axisValue ?? rows[0]?.data?.[2]);
if (!Number.isFinite(target)) return "";
emitCursorAt(target);
return cursorRows(target)
.map(
(row: any) =>
`${row.pointName}<br/>${displayValue(row.value) ?? "—"} ${row.unit ?? ""} · ${row.quality}`,
)
.join("<br/>\n");
formatter: (param: any) => {
const row = param?.data ?? [];
if (row[1] === null || row[1] === undefined) return "";
const unit = row[5] ? ` ${row[5]}` : "";
const reason = row[8] ? `<br/>原因:${row[8]}` : "";
return [
`${row[6] ?? param.seriesName}`,
`时间:${displayDateTime(row[2] ?? row[0])}`,
`数值:${displayValue(row[3]) ?? "—"}${unit}`,
`质量:${row[4] ?? "none"}${reason}`,
].join("<br/>");
},
},
xAxis: {
@@ -424,7 +467,11 @@ function render() {
},
true,
);
if (lastCursorTime !== null) scheduleVisualCursor(lastCursorTime);
const target =
lastCursorTime === null
? midpointCursorTime()
: clampCursorTime(lastCursorTime);
emitCursorAt(target);
}
onMounted(() => {
chart = echarts.init(el.value!);
@@ -433,8 +480,13 @@ onMounted(() => {
if (chart && lastCursorTime !== null) scheduleVisualCursor(lastCursorTime);
});
observer.observe(el.value!);
chart.getZr().on("mousedown", handleMouseDown);
chart.getZr().on("mousemove", handleMouseMove);
chart.getZr().on("mouseup", stopCursorDrag);
chart.getZr().on("globalout", stopCursorDrag);
chart.on("legendselectchanged", handleLegendChange);
document.addEventListener("mouseup", stopCursorDrag);
window.addEventListener("blur", stopCursorDrag);
render();
});
watch(
@@ -445,8 +497,13 @@ watch(
onBeforeUnmount(() => {
if (cursorFrame !== undefined) cancelAnimationFrame(cursorFrame);
observer?.disconnect();
chart?.getZr().off("mousedown", handleMouseDown);
chart?.getZr().off("mousemove", handleMouseMove);
chart?.getZr().off("mouseup", stopCursorDrag);
chart?.getZr().off("globalout", stopCursorDrag);
chart?.off("legendselectchanged", handleLegendChange);
document.removeEventListener("mouseup", stopCursorDrag);
window.removeEventListener("blur", stopCursorDrag);
chart?.dispose();
});
</script>
+5 -6
View File
@@ -34,9 +34,9 @@ const form = reactive({
address: "",
data_type: "REAL",
unit: "",
collect_interval: 1,
collect_interval: 10,
store_history: true,
history_interval: 1,
history_interval: 10,
});
async function load() {
try {
@@ -106,9 +106,9 @@ function open(p?: any) {
address: "",
data_type: "REAL",
unit: "",
collect_interval: 1,
collect_interval: 10,
store_history: true,
history_interval: 1,
history_interval: 10,
},
);
if (!form.group_name && groups.value.length)
@@ -321,8 +321,7 @@ function formatTime(value?: string) {
>地址<input
v-model="form.address"
class="input"
required
placeholder="DB2.1186.0" /></label
required /></label
><label class="field"
>单位<input v-model="form.unit" class="input" maxlength="32"
/></label>
+58 -20
View File
@@ -12,7 +12,7 @@ type Device = {
connection_status: string;
last_online_at?: string;
last_offline_at?: string;
protocol_config: Record<string, number>;
protocol_config: Record<string, number | string>;
};
const items = ref<Device[]>([]),
loading = ref(false),
@@ -25,11 +25,24 @@ const form = reactive({
host: "192.168.107.10",
port: 102,
connect_timeout: 5,
reconnect_interval: 10,
reconnect_interval: 5,
enabled: true,
rack: 0,
slot: 0,
unit_id: 1,
float32_order: "CDAB",
});
function applyProtocolDefaults() {
if (form.protocol_type === "S7") {
form.port = 102;
form.rack = 0;
form.slot = 0;
return;
}
form.port = 502;
form.unit_id = 1;
form.float32_order = "CDAB";
}
async function load() {
loading.value = true;
try {
@@ -49,22 +62,26 @@ function open(d?: Device) {
Object.assign(
form,
d
? {
...d,
rack: d.protocol_config.rack ?? 0,
slot: d.protocol_config.slot ?? 0,
}
: {
name: "",
protocol_type: "S7",
host: "192.168.107.10",
port: 102,
connect_timeout: 5,
reconnect_interval: 10,
enabled: true,
rack: 0,
slot: 0,
},
? {
...d,
rack: d.protocol_config.rack ?? 0,
slot: d.protocol_config.slot ?? 0,
unit_id: d.protocol_config.unit_id ?? 1,
float32_order: String(d.protocol_config.float32_order ?? "CDAB"),
}
: {
name: "",
protocol_type: "S7",
host: "192.168.107.10",
port: 102,
connect_timeout: 5,
reconnect_interval: 5,
enabled: true,
rack: 0,
slot: 0,
unit_id: 1,
float32_order: "CDAB",
},
);
show.value = true;
}
@@ -74,7 +91,7 @@ async function save() {
protocol_config:
form.protocol_type === "S7"
? { rack: form.rack, slot: form.slot }
: { unit_id: 1, float32_order: "ABCD" },
: { unit_id: form.unit_id, float32_order: form.float32_order },
};
try {
editing.value
@@ -212,7 +229,11 @@ function formatTime(value?: string) {
required
maxlength="128" /></label
><label class="field"
>协议<select v-model="form.protocol_type" class="select">
>协议<select
v-model="form.protocol_type"
class="select"
@change="applyProtocolDefaults"
>
<option>S7</option>
<option>MODBUS_TCP</option>
</select></label
@@ -236,6 +257,23 @@ function formatTime(value?: string) {
v-model.number="form.slot"
class="input"
type="number" /></label></template
><template v-else
><label class="field"
>站号<input
v-model.number="form.unit_id"
class="input"
type="number"
min="1"
max="247"
required /></label
><label class="field"
>REAL 字节序<select v-model="form.float32_order" class="select">
<option>ABCD</option>
<option>BADC</option>
<option>CDAB</option>
<option>DCBA</option>
</select></label
></template
><label class="field"
>连接超时<input
v-model.number="form.connect_timeout"
+25 -7
View File
@@ -5,7 +5,19 @@ import HistoryChart from "@/components/charts/HistoryChart.vue";
import { historyApi, systemApi } from "@/api/platform";
const groups = ref<any[]>([]);
const selected = ref<string[]>([]);
const historySelectionKey = "aqua.history.selectedPointIds";
function readStoredSelection() {
try {
const raw = sessionStorage.getItem(historySelectionKey);
const parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed)
? parsed.filter((id) => typeof id === "string")
: [];
} catch {
return [];
}
}
const selected = ref<string[]>(readStoredSelection());
const expandedGroups = ref<Set<string>>(new Set());
const mode = ref<"curve" | "table">("curve");
const range = ref("1h");
@@ -17,6 +29,7 @@ const cursor = ref<any[]>([]);
const retention = ref(365);
const loading = ref(false);
const treeLoading = ref(false);
const treeReady = ref(false);
const cleaning = ref(false);
const queryError = ref("");
const cache = new Map<string, any>();
@@ -115,10 +128,7 @@ function syncTree(nextGroups: any[]) {
.flatMap((g: any) => g.children ?? [])
.filter((p: any) => p.type === "collection" && !p.disabled);
const ids = new Set(points.map((p: any) => p.id));
const preserved = selected.value.filter((id) => ids.has(id));
selected.value = preserved.length
? preserved
: points.slice(0, 2).map((p: any) => p.id);
selected.value = selected.value.filter((id) => ids.has(id));
const open = new Set(
nextGroups
.filter((g: any) => g.children?.some((p: any) => p.type === "collection"))
@@ -139,6 +149,7 @@ async function loadTree() {
groups.value = [];
selected.value = [];
} finally {
treeReady.value = true;
treeLoading.value = false;
}
}
@@ -276,6 +287,7 @@ function tableValue(value: any) {
watch(
() => [
treeReady.value,
selected.value.join(","),
start.value.getTime(),
end.value.getTime(),
@@ -283,11 +295,18 @@ watch(
interval.value,
],
() => {
if (!treeReady.value) return;
if (queryTimer) clearTimeout(queryTimer);
queryTimer = setTimeout(() => void query(), 180);
},
{ immediate: true },
);
watch(
() => selected.value,
(value) => {
sessionStorage.setItem(historySelectionKey, JSON.stringify(value));
},
);
onMounted(async () => {
try {
const r: any = await systemApi.getRetention();
@@ -501,8 +520,7 @@ onBeforeUnmount(() => {
<td>{{ c.pointName }}</td>
<td>{{ formatDateTime(c.ts) }}</td>
<td :class="c.quality === 'bad' ? 'quality-bad' : ''">
{{ c.interpolated ? "" : ""
}}{{ formatCursorValue(c.value) ?? "—" }}
{{ formatCursorValue(c.value) ?? "" }}
{{ c.unit }}
</td>
<td>{{ c.quality ?? "—" }}</td>
+4 -3
View File
@@ -251,13 +251,15 @@ onMounted(async () => {
<div class="toolbar" style="margin-bottom: 12px">
<button
:class="['btn', tab === 'points' && 'btn-primary']"
v-if="tab === 'logs'"
class="btn"
@click="tab = 'points'"
>
写入点
</button>
<button
:class="['btn', tab === 'logs' && 'btn-primary']"
v-if="tab === 'points'"
class="btn"
@click="tab = 'logs'"
>
操作日志
@@ -380,7 +382,6 @@ onMounted(async () => {
v-model="form.address"
class="input"
required
placeholder="MD540"
/>
</label>
<label class="field">