里程点补充
This commit is contained in:
@@ -50,6 +50,8 @@ export interface RosInspectionRecordLineImage {
|
|||||||
poseY?: number;
|
poseY?: number;
|
||||||
/** 抓拍时的小车朝向 */
|
/** 抓拍时的小车朝向 */
|
||||||
poseYaw?: number;
|
poseYaw?: number;
|
||||||
|
/** 告警位置对应的里程点标识(后端按告警位姿在巡检路径上投影匹配) */
|
||||||
|
mileageLabel?: string;
|
||||||
/** 识别状态:0待执行 1执行中 2执行成功 3执行失败 */
|
/** 识别状态:0待执行 1执行中 2执行成功 3执行失败 */
|
||||||
recognitionStatus?: number;
|
recognitionStatus?: number;
|
||||||
/** AI识别提示词 */
|
/** AI识别提示词 */
|
||||||
|
|||||||
49
src/api/mileage.ts
Normal file
49
src/api/mileage.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
import type { ApiResult } from "@/types/api";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 里程点(设备级标定物,独立于巡检任务与轮次)
|
||||||
|
* 对应后端 com.haizhiyustc.ros.entity.RosMileagePoint
|
||||||
|
*/
|
||||||
|
export interface RosMileagePoint {
|
||||||
|
/** 主键id,新增时不传 */
|
||||||
|
id?: string;
|
||||||
|
/** 所属巡检设备id */
|
||||||
|
carId: string;
|
||||||
|
/** 里程点标识,人工填写,如 a区-01 */
|
||||||
|
mileageLabel: string;
|
||||||
|
/** 位姿x,单位米 */
|
||||||
|
poseX?: number;
|
||||||
|
/** 位姿y,单位米 */
|
||||||
|
poseY?: number;
|
||||||
|
/** 备注 */
|
||||||
|
remark?: string;
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime?: string;
|
||||||
|
/** 更新时间 */
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询指定设备的里程点列表
|
||||||
|
export function getMileagePointList(carId: string) {
|
||||||
|
return request<ApiResult<RosMileagePoint[]>>({
|
||||||
|
url: "/admin/rosMileagePoint/list",
|
||||||
|
params: { carId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存里程点(有 id 为更新,无 id 为新增)
|
||||||
|
export function saveMileagePoint(data: RosMileagePoint) {
|
||||||
|
return request<ApiResult<RosMileagePoint>>({
|
||||||
|
url: "/admin/rosMileagePoint/save",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除里程点
|
||||||
|
export function removeMileagePoint(id: string) {
|
||||||
|
return request<ApiResult<null>>({
|
||||||
|
url: "/admin/rosMileagePoint/remove",
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ interface AlarmRecord {
|
|||||||
status: RecognitionStatusText;
|
status: RecognitionStatusText;
|
||||||
time: string;
|
time: string;
|
||||||
position: string;
|
position: string;
|
||||||
|
mileage: string;
|
||||||
result: string;
|
result: string;
|
||||||
warning: boolean;
|
warning: boolean;
|
||||||
}
|
}
|
||||||
@@ -153,6 +154,7 @@ function normalizeAlarmRecord(record: RosInspectionRecordLineImage, index: numbe
|
|||||||
status: getRecognitionStatusText(record.recognitionStatus),
|
status: getRecognitionStatusText(record.recognitionStatus),
|
||||||
time: record.captureTime || record.createTime || "--",
|
time: record.captureTime || record.createTime || "--",
|
||||||
position: formatPosition(record),
|
position: formatPosition(record),
|
||||||
|
mileage: record.mileageLabel || "--",
|
||||||
result: record.warningValue || record.recognitionResult || record.failReason || "--",
|
result: record.warningValue || record.recognitionResult || record.failReason || "--",
|
||||||
warning: Boolean(record.warning),
|
warning: Boolean(record.warning),
|
||||||
};
|
};
|
||||||
@@ -271,19 +273,20 @@ onMounted(() => {
|
|||||||
<th>案件状态</th>
|
<th>案件状态</th>
|
||||||
<th>告警时间</th>
|
<th>告警时间</th>
|
||||||
<th>位置</th>
|
<th>位置</th>
|
||||||
|
<th>里程点</th>
|
||||||
<th>识别结果</th>
|
<th>识别结果</th>
|
||||||
<th>是否预警</th>
|
<th>是否预警</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="listLoading">
|
<tr v-if="listLoading">
|
||||||
<td class="table-empty" colspan="9">近期告警加载中...</td>
|
<td class="table-empty" colspan="10">近期告警加载中...</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else-if="listError">
|
<tr v-else-if="listError">
|
||||||
<td class="table-empty" colspan="9">{{ listError }}</td>
|
<td class="table-empty" colspan="10">{{ listError }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else-if="!pagedAlarms.length">
|
<tr v-else-if="!pagedAlarms.length">
|
||||||
<td class="table-empty" colspan="9">暂无告警记录</td>
|
<td class="table-empty" colspan="10">暂无告警记录</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="alarm in pagedAlarms" v-else :key="alarm.id">
|
<tr v-for="alarm in pagedAlarms" v-else :key="alarm.id">
|
||||||
<td>{{ alarm.id }}</td>
|
<td>{{ alarm.id }}</td>
|
||||||
@@ -305,6 +308,7 @@ onMounted(() => {
|
|||||||
</td>
|
</td>
|
||||||
<td>{{ alarm.time }}</td>
|
<td>{{ alarm.time }}</td>
|
||||||
<td>{{ alarm.position }}</td>
|
<td>{{ alarm.position }}</td>
|
||||||
|
<td>{{ alarm.mileage }}</td>
|
||||||
<td>{{ alarm.result }}</td>
|
<td>{{ alarm.result }}</td>
|
||||||
<td>{{ alarm.warning ? "是" : "否" }}</td>
|
<td>{{ alarm.warning ? "是" : "否" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ import {
|
|||||||
updateRosCarMapUrls,
|
updateRosCarMapUrls,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
} from "@/api/rosCar";
|
} from "@/api/rosCar";
|
||||||
|
import {
|
||||||
|
getMileagePointList,
|
||||||
|
removeMileagePoint,
|
||||||
|
saveMileagePoint,
|
||||||
|
type RosMileagePoint,
|
||||||
|
} from "@/api/mileage";
|
||||||
import { parseRosMapYaml } from "@/utils/rosMapYaml";
|
import { parseRosMapYaml } from "@/utils/rosMapYaml";
|
||||||
|
|
||||||
type DeviceStatus = "空闲" | "巡检" | "离线" | "充电中";
|
type DeviceStatus = "空闲" | "巡检" | "离线" | "充电中";
|
||||||
@@ -20,7 +26,9 @@ type RecognitionActionCode =
|
|||||||
| "TUNNEL_WATER"
|
| "TUNNEL_WATER"
|
||||||
| "WALL_CRACK";
|
| "WALL_CRACK";
|
||||||
type PatrolTaskMode = "定时" | "周期" | "临时";
|
type PatrolTaskMode = "定时" | "周期" | "临时";
|
||||||
type EditorMode = "select" | "normal" | "thermal" | "pump" | "waypoint" | "dock" | "preDock";
|
type EditorMode = "select" | "normal" | "thermal" | "pump" | "waypoint" | "dock" | "preDock" | "mileage";
|
||||||
|
/** 地图编辑目标:巡检轮次(编辑巡检点位)或里程点(独立标定物),两者互斥 */
|
||||||
|
type MapEditTarget = "patrol" | "mileage";
|
||||||
|
|
||||||
interface RosCarRecord {
|
interface RosCarRecord {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -214,6 +222,20 @@ interface PatrolTask {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 里程点:设备级标定物,独立于巡检点位与轮次,仅用于告警位置参照
|
||||||
|
*/
|
||||||
|
interface MileagePointState {
|
||||||
|
/** 后端主键id,未保存时为空 */
|
||||||
|
id?: string;
|
||||||
|
/** 里程点标识,人工填写,如 a区-01 */
|
||||||
|
label: string;
|
||||||
|
/** 地图百分比坐标x */
|
||||||
|
x: number;
|
||||||
|
/** 地图百分比坐标y */
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface DockConfigState {
|
interface DockConfigState {
|
||||||
dockX?: number;
|
dockX?: number;
|
||||||
dockY?: number;
|
dockY?: number;
|
||||||
@@ -236,6 +258,8 @@ interface DeviceConfigState {
|
|||||||
points: PatrolPoint[];
|
points: PatrolPoint[];
|
||||||
selectedPointIds: string[];
|
selectedPointIds: string[];
|
||||||
plans: PatrolTask[];
|
plans: PatrolTask[];
|
||||||
|
/** 设备级里程点列表(独立接口维护,不随巡检任务保存) */
|
||||||
|
mileages: MileagePointState[];
|
||||||
dock: DockConfigState;
|
dock: DockConfigState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +336,26 @@ const pointDropPosition = ref<"before" | "after">("before");
|
|||||||
// 列表点击"前/后插入"后,记录待插入位置,等地图点选坐标后写入该位置
|
// 列表点击"前/后插入"后,记录待插入位置,等地图点选坐标后写入该位置
|
||||||
const pendingInsertIndex = ref<number | null>(null);
|
const pendingInsertIndex = ref<number | null>(null);
|
||||||
const pendingInsertLabel = ref("");
|
const pendingInsertLabel = ref("");
|
||||||
|
// 地图当前编辑对象,默认编辑巡检轮次点位
|
||||||
|
const mapEditTarget = ref<MapEditTarget>("patrol");
|
||||||
|
// 里程点拖动状态
|
||||||
|
const dragMileageState = reactive({
|
||||||
|
active: false,
|
||||||
|
moved: false,
|
||||||
|
mileageId: "",
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
});
|
||||||
|
// 里程点编辑弹窗状态
|
||||||
|
const mileageDialogVisible = ref(false);
|
||||||
|
const mileageSaving = ref(false);
|
||||||
|
const mileageMessage = ref("");
|
||||||
|
const mileageForm = reactive({
|
||||||
|
id: "",
|
||||||
|
label: "",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
const mapStageRef = ref<HTMLElement | null>(null);
|
const mapStageRef = ref<HTMLElement | null>(null);
|
||||||
const mapCanvasRef = ref<HTMLCanvasElement | null>(null);
|
const mapCanvasRef = ref<HTMLCanvasElement | null>(null);
|
||||||
const mapOverviewRef = ref<HTMLCanvasElement | null>(null);
|
const mapOverviewRef = ref<HTMLCanvasElement | null>(null);
|
||||||
@@ -406,6 +450,9 @@ const selectedPointHighlight = {
|
|||||||
glow: "rgba(255, 178, 110, 0.32)",
|
glow: "rgba(255, 178, 110, 0.32)",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 里程点独立配色(设备级标定物,不参与巡检路径)
|
||||||
|
const mileagePointColor = "#c39bff";
|
||||||
|
|
||||||
const defaultRosMapMeta: RosMapMetaState = {
|
const defaultRosMapMeta: RosMapMetaState = {
|
||||||
image: "room.png",
|
image: "room.png",
|
||||||
resolution: 0.05,
|
resolution: 0.05,
|
||||||
@@ -1287,6 +1334,7 @@ function buildDeviceConfig(device: DeviceRecord): DeviceConfigState {
|
|||||||
points,
|
points,
|
||||||
selectedPointIds: [],
|
selectedPointIds: [],
|
||||||
plans,
|
plans,
|
||||||
|
mileages: [],
|
||||||
dock: buildDockConfig(device),
|
dock: buildDockConfig(device),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1476,11 +1524,14 @@ async function openConfigDialog(device: DeviceRecord) {
|
|||||||
|
|
||||||
await applyDeviceMapUrls(device, config);
|
await applyDeviceMapUrls(device, config);
|
||||||
config.points = config.points.map(normalizePatrolPointActions);
|
config.points = config.points.map(normalizePatrolPointActions);
|
||||||
|
// 兼容热更新前生成的旧配置对象,避免里程点列表未初始化导致渲染异常
|
||||||
|
config.mileages = config.mileages ?? [];
|
||||||
deviceConfigs[device.id] = config;
|
deviceConfigs[device.id] = config;
|
||||||
configuringDeviceId.value = device.id;
|
configuringDeviceId.value = device.id;
|
||||||
configDialogVisible.value = true;
|
configDialogVisible.value = true;
|
||||||
mapEditorMode.value = "select";
|
mapEditorMode.value = "select";
|
||||||
cancelInsertPoint();
|
cancelInsertPoint();
|
||||||
|
void loadMileagePoints(device.id, config);
|
||||||
|
|
||||||
if (!config.plans.some((plan) => plan.id === config.activePlanId)) {
|
if (!config.plans.some((plan) => plan.id === config.activePlanId)) {
|
||||||
config.activePlanId = config.plans[0]?.id ?? "";
|
config.activePlanId = config.plans[0]?.id ?? "";
|
||||||
@@ -1521,10 +1572,12 @@ function closeConfigDialog() {
|
|||||||
|
|
||||||
configDialogVisible.value = false;
|
configDialogVisible.value = false;
|
||||||
planDialogVisible.value = false;
|
planDialogVisible.value = false;
|
||||||
|
mileageDialogVisible.value = false;
|
||||||
configuringDeviceId.value = null;
|
configuringDeviceId.value = null;
|
||||||
cancelInsertPoint();
|
cancelInsertPoint();
|
||||||
clearSelectionBox();
|
clearSelectionBox();
|
||||||
clearDragPointState();
|
clearDragPointState();
|
||||||
|
clearDragMileageState();
|
||||||
clearMapPanState();
|
clearMapPanState();
|
||||||
stopMapSyncTimer();
|
stopMapSyncTimer();
|
||||||
}
|
}
|
||||||
@@ -1832,6 +1885,30 @@ function findHitPoint(event: MouseEvent) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 里程点命中检测,用于里程模式下的拖动与编辑
|
||||||
|
function findHitMileagePoint(event: MouseEvent) {
|
||||||
|
const rect = getMapStageRect();
|
||||||
|
if (!rect || !currentConfig.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pointerX = event.clientX - rect.left;
|
||||||
|
const pointerY = event.clientY - rect.top;
|
||||||
|
const mileages = [...currentConfig.value.mileages].reverse();
|
||||||
|
|
||||||
|
for (const mileage of mileages) {
|
||||||
|
const pixelPoint = percentToStagePixels(mileage.x, mileage.y, rect);
|
||||||
|
const deltaX = pixelPoint.x - pointerX;
|
||||||
|
const deltaY = pixelPoint.y - pointerY;
|
||||||
|
|
||||||
|
if (Math.hypot(deltaX, deltaY) <= 16) {
|
||||||
|
return mileage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function renderMapCanvas() {
|
function renderMapCanvas() {
|
||||||
const canvas = mapCanvasRef.value;
|
const canvas = mapCanvasRef.value;
|
||||||
const rect = getMapStageRect();
|
const rect = getMapStageRect();
|
||||||
@@ -2003,6 +2080,37 @@ function renderMapCanvas() {
|
|||||||
context.restore();
|
context.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 里程点用菱形 + 标识文字绘制,与巡检点位区分
|
||||||
|
currentConfig.value?.mileages.forEach((mileage) => {
|
||||||
|
const pixelPoint = percentToStagePixels(mileage.x, mileage.y, rect);
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(pixelPoint.x, pixelPoint.y - 9);
|
||||||
|
context.lineTo(pixelPoint.x + 9, pixelPoint.y);
|
||||||
|
context.lineTo(pixelPoint.x, pixelPoint.y + 9);
|
||||||
|
context.lineTo(pixelPoint.x - 9, pixelPoint.y);
|
||||||
|
context.closePath();
|
||||||
|
context.fillStyle = "rgba(5, 18, 38, 0.94)";
|
||||||
|
context.fill();
|
||||||
|
context.lineWidth = 2;
|
||||||
|
context.strokeStyle = mileagePointColor;
|
||||||
|
context.shadowColor = "rgba(195, 155, 255, 0.35)";
|
||||||
|
context.shadowBlur = 12;
|
||||||
|
context.stroke();
|
||||||
|
context.restore();
|
||||||
|
|
||||||
|
if (mileage.label) {
|
||||||
|
context.save();
|
||||||
|
context.fillStyle = "#e6d6ff";
|
||||||
|
context.font = '600 11px "Microsoft YaHei", sans-serif';
|
||||||
|
context.textAlign = "center";
|
||||||
|
context.textBaseline = "bottom";
|
||||||
|
context.fillText(mileage.label, pixelPoint.x, pixelPoint.y - 12);
|
||||||
|
context.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
drawDockMarker(
|
drawDockMarker(
|
||||||
context,
|
context,
|
||||||
rect,
|
rect,
|
||||||
@@ -2289,6 +2397,14 @@ function clearSelectionBox() {
|
|||||||
selectionBox.endY = 0;
|
selectionBox.endY = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearDragMileageState() {
|
||||||
|
dragMileageState.active = false;
|
||||||
|
dragMileageState.moved = false;
|
||||||
|
dragMileageState.mileageId = "";
|
||||||
|
dragMileageState.offsetX = 0;
|
||||||
|
dragMileageState.offsetY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
function clearDragPointState() {
|
function clearDragPointState() {
|
||||||
dragPointState.active = false;
|
dragPointState.active = false;
|
||||||
dragPointState.moved = false;
|
dragPointState.moved = false;
|
||||||
@@ -2353,6 +2469,30 @@ function handleMapPointerDown(event: MouseEvent) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 里程模式:只操作里程点,命中则进入拖动,未命中则平移地图
|
||||||
|
if (mapEditTarget.value === "mileage") {
|
||||||
|
const hitMileage = findHitMileagePoint(event);
|
||||||
|
|
||||||
|
if (hitMileage) {
|
||||||
|
dragMileageState.active = true;
|
||||||
|
dragMileageState.moved = false;
|
||||||
|
dragMileageState.mileageId = hitMileage.id ?? "";
|
||||||
|
dragMileageState.offsetX = hitMileage.x - position.x;
|
||||||
|
dragMileageState.offsetY = hitMileage.y - position.y;
|
||||||
|
bindMapPointerTracking();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapPanState.active = true;
|
||||||
|
mapPanState.moved = false;
|
||||||
|
mapPanState.startClientX = event.clientX;
|
||||||
|
mapPanState.startClientY = event.clientY;
|
||||||
|
mapPanState.startPanX = mapPan.x;
|
||||||
|
mapPanState.startPanY = mapPan.y;
|
||||||
|
bindMapPointerTracking();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const hitPoint = findHitPoint(event);
|
const hitPoint = findHitPoint(event);
|
||||||
if (hitPoint) {
|
if (hitPoint) {
|
||||||
if (!currentConfig.value.selectedPointIds.includes(hitPoint.id)) {
|
if (!currentConfig.value.selectedPointIds.includes(hitPoint.id)) {
|
||||||
@@ -2392,6 +2532,39 @@ function handleMapPointerMove(event: MouseEvent) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dragMileageState.active && currentConfig.value) {
|
||||||
|
const position = getRelativePosition(event);
|
||||||
|
if (!position) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetMileage = currentConfig.value.mileages.find(
|
||||||
|
(mileage) => mileage.id === dragMileageState.mileageId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!targetMileage) {
|
||||||
|
clearDragMileageState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPosition = clampMapPosition(
|
||||||
|
position.x + dragMileageState.offsetX,
|
||||||
|
position.y + dragMileageState.offsetY,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
Math.abs(nextPosition.x - targetMileage.x) > 0.05 ||
|
||||||
|
Math.abs(nextPosition.y - targetMileage.y) > 0.05
|
||||||
|
) {
|
||||||
|
dragMileageState.moved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetMileage.x = nextPosition.x;
|
||||||
|
targetMileage.y = nextPosition.y;
|
||||||
|
scheduleMapCanvasRender();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (dragPointState.active && currentConfig.value) {
|
if (dragPointState.active && currentConfig.value) {
|
||||||
const position = getRelativePosition(event);
|
const position = getRelativePosition(event);
|
||||||
if (!position) {
|
if (!position) {
|
||||||
@@ -2455,6 +2628,24 @@ function handleMapPointerUp(event: MouseEvent) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dragMileageState.active) {
|
||||||
|
const mileageId = dragMileageState.mileageId;
|
||||||
|
const moved = dragMileageState.moved;
|
||||||
|
|
||||||
|
clearDragMileageState();
|
||||||
|
|
||||||
|
if (moved) {
|
||||||
|
suppressStageClick.value = true;
|
||||||
|
const mileage = currentConfig.value?.mileages.find((item) => item.id === mileageId);
|
||||||
|
|
||||||
|
if (mileage) {
|
||||||
|
void persistMileagePosition(mileage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (dragPointState.active) {
|
if (dragPointState.active) {
|
||||||
if (dragPointState.moved) {
|
if (dragPointState.moved) {
|
||||||
suppressStageClick.value = true;
|
suppressStageClick.value = true;
|
||||||
@@ -2721,6 +2912,13 @@ function handleMapStageClick(event: MouseEvent) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 里程模式:点中已有里程点则编辑,点空白处新增
|
||||||
|
if (mapEditTarget.value === "mileage") {
|
||||||
|
const hitMileage = findHitMileagePoint(event);
|
||||||
|
openMileageDialog(hitMileage ? undefined : position, hitMileage ?? undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const hitPoint = findHitPoint(event);
|
const hitPoint = findHitPoint(event);
|
||||||
if (hitPoint) {
|
if (hitPoint) {
|
||||||
handlePointClick(hitPoint.id, event);
|
handlePointClick(hitPoint.id, event);
|
||||||
@@ -2904,6 +3102,170 @@ function clearCanvas() {
|
|||||||
updateSelection([]);
|
updateSelection([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 里程点按设备维度独立加载,不随巡检任务一起保存
|
||||||
|
async function loadMileagePoints(deviceId: string, config: DeviceConfigState) {
|
||||||
|
try {
|
||||||
|
const response = await getMileagePointList(deviceId);
|
||||||
|
const rows: RosMileagePoint[] = response.data ?? [];
|
||||||
|
|
||||||
|
config.mileages = rows.map((item) => {
|
||||||
|
const position = rosPoseToMapPercent(item.poseX ?? 0, item.poseY ?? 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
label: item.mileageLabel,
|
||||||
|
x: position.x,
|
||||||
|
y: position.y,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
nextTick(() => {
|
||||||
|
scheduleMapCanvasRender();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
config.mileages = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换地图编辑目标:里程模式下只操作里程点,巡检模式下只操作巡检点位
|
||||||
|
function switchMapEditTarget(target: MapEditTarget) {
|
||||||
|
mapEditTarget.value = target;
|
||||||
|
cancelInsertPoint();
|
||||||
|
|
||||||
|
if (target === "mileage") {
|
||||||
|
mapEditorMode.value = "mileage";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapEditorMode.value === "mileage") {
|
||||||
|
mapEditorMode.value = "select";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拖动结束后把里程点新位置写回后端
|
||||||
|
async function persistMileagePosition(mileage: MileagePointState) {
|
||||||
|
const device = currentConfiguredDevice.value;
|
||||||
|
|
||||||
|
if (!device || !mileage.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rosPosition = percentToRosPosition(mileage.x, mileage.y);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await saveMileagePoint({
|
||||||
|
id: mileage.id,
|
||||||
|
carId: device.id,
|
||||||
|
mileageLabel: mileage.label,
|
||||||
|
poseX: rosPosition.x,
|
||||||
|
poseY: rosPosition.y,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开里程点编辑弹窗;position 为地图点选得到的百分比坐标
|
||||||
|
function openMileageDialog(
|
||||||
|
position?: { x: number; y: number },
|
||||||
|
mileage?: MileagePointState,
|
||||||
|
) {
|
||||||
|
mileageForm.id = mileage?.id ?? "";
|
||||||
|
mileageForm.label = mileage?.label ?? "";
|
||||||
|
mileageForm.x = mileage?.x ?? position?.x ?? 0;
|
||||||
|
mileageForm.y = mileage?.y ?? position?.y ?? 0;
|
||||||
|
mileageMessage.value = "";
|
||||||
|
mileageDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMileageDialog() {
|
||||||
|
mileageDialogVisible.value = false;
|
||||||
|
mileageMessage.value = "";
|
||||||
|
mileageSaving.value = false;
|
||||||
|
// 保持当前编辑目标:里程模式下继续可点选,巡检模式下回到查看
|
||||||
|
mapEditorMode.value = mapEditTarget.value === "mileage" ? "mileage" : "select";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存里程点:把页面百分比坐标换算成 ROS 米制坐标后提交
|
||||||
|
async function saveMileage() {
|
||||||
|
const config = currentConfig.value;
|
||||||
|
const device = currentConfiguredDevice.value;
|
||||||
|
|
||||||
|
if (!config || !device || mileageSaving.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = mileageForm.label.trim();
|
||||||
|
|
||||||
|
if (!label) {
|
||||||
|
mileageMessage.value = "请填写里程点标识,例如 a区-01";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rosPosition = percentToRosPosition(mileageForm.x, mileageForm.y);
|
||||||
|
|
||||||
|
mileageSaving.value = true;
|
||||||
|
mileageMessage.value = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await saveMileagePoint({
|
||||||
|
id: mileageForm.id || undefined,
|
||||||
|
carId: device.id,
|
||||||
|
mileageLabel: label,
|
||||||
|
poseX: rosPosition.x,
|
||||||
|
poseY: rosPosition.y,
|
||||||
|
});
|
||||||
|
const saved = response.data;
|
||||||
|
|
||||||
|
if (saved?.id) {
|
||||||
|
const position = rosPoseToMapPercent(saved.poseX ?? 0, saved.poseY ?? 0);
|
||||||
|
const nextMileage: MileagePointState = {
|
||||||
|
id: saved.id,
|
||||||
|
label: saved.mileageLabel || label,
|
||||||
|
x: position.x,
|
||||||
|
y: position.y,
|
||||||
|
};
|
||||||
|
const index = config.mileages.findIndex((item) => item.id === saved.id);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
config.mileages[index] = nextMileage;
|
||||||
|
} else {
|
||||||
|
config.mileages.push(nextMileage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mileageDialogVisible.value = false;
|
||||||
|
mileageMessage.value = "";
|
||||||
|
mapEditorMode.value = mapEditTarget.value === "mileage" ? "mileage" : "select";
|
||||||
|
nextTick(() => {
|
||||||
|
scheduleMapCanvasRender();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
mileageMessage.value = "里程点保存失败,请稍后重试";
|
||||||
|
} finally {
|
||||||
|
mileageSaving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeMileagePointItem(mileage: MileagePointState) {
|
||||||
|
const config = currentConfig.value;
|
||||||
|
|
||||||
|
if (!config || !mileage.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeMileagePoint(mileage.id);
|
||||||
|
config.mileages = config.mileages.filter((item) => item.id !== mileage.id);
|
||||||
|
nextTick(() => {
|
||||||
|
scheduleMapCanvasRender();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveConfigDialog() {
|
async function saveConfigDialog() {
|
||||||
if (!currentConfig.value || !currentConfiguredDevice.value || saveProgressVisible.value) {
|
if (!currentConfig.value || !currentConfiguredDevice.value || saveProgressVisible.value) {
|
||||||
return;
|
return;
|
||||||
@@ -3459,23 +3821,40 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="editor-toolbar">
|
<div class="editor-toolbar">
|
||||||
<button class="editor-tool-btn icon" :class="{ active: mapEditorMode === 'select' }" type="button"
|
<div class="map-edit-target-switch">
|
||||||
title="拖动查看" aria-label="拖动查看" @click="cancelInsertPoint(); mapEditorMode = 'select'">
|
<button :class="{ active: mapEditTarget === 'patrol' }" type="button"
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
@click="switchMapEditTarget('patrol')">
|
||||||
<path
|
巡检轮次
|
||||||
d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z" />
|
</button>
|
||||||
</svg>
|
<button :class="{ active: mapEditTarget === 'mileage' }" type="button"
|
||||||
</button>
|
@click="switchMapEditTarget('mileage')">
|
||||||
<button v-for="mode in editorModes" :key="mode.id" :class="[
|
里程点
|
||||||
'editor-tool-btn',
|
</button>
|
||||||
{ active: mapEditorMode === mode.id },
|
</div>
|
||||||
]" type="button" @click="mapEditorMode = mode.id">
|
|
||||||
{{ mode.label }}
|
<template v-if="mapEditTarget === 'patrol'">
|
||||||
</button>
|
<button class="editor-tool-btn icon" :class="{ active: mapEditorMode === 'select' }" type="button"
|
||||||
<button class="editor-tool-btn" type="button" @click="clearCanvas">
|
title="拖动查看" aria-label="拖动查看" @click="cancelInsertPoint(); mapEditorMode = 'select'">
|
||||||
清空巡检点
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
</button>
|
<path
|
||||||
<div v-if="pendingInsertIndex !== null" class="editor-tip">
|
d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button v-for="mode in editorModes" :key="mode.id" :class="[
|
||||||
|
'editor-tool-btn',
|
||||||
|
{ active: mapEditorMode === mode.id },
|
||||||
|
]" type="button" @click="mapEditorMode = mode.id">
|
||||||
|
{{ mode.label }}
|
||||||
|
</button>
|
||||||
|
<button class="editor-tool-btn" type="button" @click="clearCanvas">
|
||||||
|
清空巡检点
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="mapEditTarget === 'mileage'" class="editor-tip">
|
||||||
|
点击空白处新增里程点,拖动已有点位可调整位置
|
||||||
|
</div>
|
||||||
|
<div v-else-if="pendingInsertIndex !== null" class="editor-tip">
|
||||||
在地图上点击放置新点位,将插入到{{ pendingInsertLabel }}
|
在地图上点击放置新点位,将插入到{{ pendingInsertLabel }}
|
||||||
<button class="editor-tip-cancel" type="button" @click="cancelInsertPoint">取消插入</button>
|
<button class="editor-tip-cancel" type="button" @click="cancelInsertPoint">取消插入</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -3611,6 +3990,40 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="config-side-card mileage-config-board">
|
||||||
|
<div class="panel-head inline">
|
||||||
|
<div>
|
||||||
|
<div class="panel-title">里程点配置</div>
|
||||||
|
<div class="panel-subtitle">
|
||||||
|
设备级定位标定物,独立于巡检点位与轮次,用于告警时定位故障位置
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button :class="{ primary: mapEditTarget === 'mileage' }" class="page-action-btn ghost" type="button"
|
||||||
|
@click="switchMapEditTarget('mileage')">
|
||||||
|
地图编辑
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="mapEditTarget === 'mileage'" class="mileage-pick-hint">
|
||||||
|
请在左侧地图上点击新增里程点,或拖动已有点位调整位置
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mileage-list-shell">
|
||||||
|
<div v-for="mileage in currentConfig.mileages" :key="mileage.id" class="mileage-list-item">
|
||||||
|
<div class="mileage-list-main">
|
||||||
|
<div class="mileage-list-label">{{ mileage.label || "未命名里程点" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="point-list-actions">
|
||||||
|
<button type="button" @click="openMileageDialog(undefined, mileage)">编辑</button>
|
||||||
|
<button class="danger" type="button" @click="removeMileagePointItem(mileage)">删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!currentConfig.mileages.length" class="config-empty point-list-empty">
|
||||||
|
暂无里程点,点击“地图点选新增”后在地图上选择位置。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="config-side-card dock-config-board">
|
<div class="config-side-card dock-config-board">
|
||||||
<div class="dock-config-head">
|
<div class="dock-config-head">
|
||||||
<span class="dock-config-title">充电回充配置</span>
|
<span class="dock-config-title">充电回充配置</span>
|
||||||
@@ -3772,6 +4185,36 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="mileageDialogVisible" class="plan-dialog-mask" @click.self="closeMileageDialog">
|
||||||
|
<div class="plan-dialog-card">
|
||||||
|
<div class="panel-head inline">
|
||||||
|
<div>
|
||||||
|
<div class="panel-title">里程点</div>
|
||||||
|
<div class="panel-subtitle">填写里程标识后保存,例如 a区-01</div>
|
||||||
|
</div>
|
||||||
|
<button class="dialog-close" type="button" @click="closeMileageDialog">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-form-grid compact plan-dialog-grid">
|
||||||
|
<label class="config-field wide">
|
||||||
|
<span>里程标识</span>
|
||||||
|
<input v-model="mileageForm.label" placeholder="例如:a区-01" type="text" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="mileageMessage" class="map-upload-message">{{ mileageMessage }}</div>
|
||||||
|
|
||||||
|
<div class="plan-editor-actions">
|
||||||
|
<button class="page-action-btn ghost" type="button" @click="closeMileageDialog">
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button class="page-action-btn primary" :disabled="mileageSaving" type="button" @click="saveMileage">
|
||||||
|
{{ mileageSaving ? "保存中..." : "保存" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="saveProgressVisible" class="save-progress-mask">
|
<div v-if="saveProgressVisible" class="save-progress-mask">
|
||||||
<div class="save-progress-card">
|
<div class="save-progress-card">
|
||||||
<div class="save-progress-title">保存</div>
|
<div class="save-progress-title">保存</div>
|
||||||
@@ -4142,6 +4585,31 @@ watch(
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.map-edit-target-switch {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px;
|
||||||
|
border: 1px solid rgba(87, 198, 255, 0.26);
|
||||||
|
background: rgba(8, 31, 62, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-edit-target-switch button {
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(192, 235, 250, 0.78);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-edit-target-switch button.active {
|
||||||
|
color: #eefcff;
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
rgba(24, 102, 180, 0.88),
|
||||||
|
rgba(35, 158, 215, 0.88));
|
||||||
|
}
|
||||||
|
|
||||||
.editor-tool-btn {
|
.editor-tool-btn {
|
||||||
height: 34px;
|
height: 34px;
|
||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
@@ -4757,8 +5225,8 @@ watch(
|
|||||||
.rounds-board {
|
.rounds-board {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
/* 新增充电回充卡片后,轮次配置不再设为 0 基准高度,避免被压缩到看不见 */
|
/* 侧栏已有充电回充、里程点两张卡片,保证轮次配置的最小可用高度 */
|
||||||
min-height: 300px;
|
min-height: 460px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -4922,6 +5390,50 @@ watch(
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mileage-config-board {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mileage-pick-hint {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px dashed rgba(195, 155, 255, 0.5);
|
||||||
|
background: rgba(60, 40, 110, 0.35);
|
||||||
|
color: #e2d4ff;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mileage-list-shell {
|
||||||
|
margin-top: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mileage-list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid rgba(174, 138, 255, 0.22);
|
||||||
|
background: rgba(30, 18, 62, 0.62);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mileage-list-main {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mileage-list-label {
|
||||||
|
color: #f2fbff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.point-drag-handle {
|
.point-drag-handle {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ interface AlertItem {
|
|||||||
title: string;
|
title: string;
|
||||||
result: string;
|
result: string;
|
||||||
time: string;
|
time: string;
|
||||||
|
mileage: string;
|
||||||
photo: string;
|
photo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,6 +399,7 @@ function normalizeAlertRecord(record: RosInspectionRecordLineImage, index: numbe
|
|||||||
title,
|
title,
|
||||||
result: record.warningValue || record.recognitionResult || record.failReason || "--",
|
result: record.warningValue || record.recognitionResult || record.failReason || "--",
|
||||||
time: record.captureTime || record.createTime || "--",
|
time: record.captureTime || record.createTime || "--",
|
||||||
|
mileage: record.mileageLabel || "",
|
||||||
photo: buildFileAccessUrl(record.imageUrl || "") || getRecentAlertPhoto(title),
|
photo: buildFileAccessUrl(record.imageUrl || "") || getRecentAlertPhoto(title),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1820,6 +1822,10 @@ function getStatusClass(status: DeviceItem["status"]) {
|
|||||||
<span class="alert-label">告警时间</span>
|
<span class="alert-label">告警时间</span>
|
||||||
<span class="alert-time">{{ alert.time }}</span>
|
<span class="alert-time">{{ alert.time }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="alert.mileage" class="alert-time-row">
|
||||||
|
<span class="alert-label">里程点</span>
|
||||||
|
<span class="alert-time">{{ alert.mileage }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user