123
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user