优化标点交互

This commit is contained in:
Chenhuaqiang
2026-10-09 17:14:05 +08:00
parent 3dc1a1d313
commit e6368e32d5
2 changed files with 225 additions and 12 deletions

View File

@@ -306,6 +306,12 @@ const configuringDeviceId = ref<string | null>(null);
const editingPointId = ref<string | null>(null); const editingPointId = ref<string | null>(null);
const formError = ref(""); const formError = ref("");
const mapEditorMode = ref<EditorMode>("select"); const mapEditorMode = ref<EditorMode>("select");
const draggingPointId = ref<string | null>(null);
const pointDropTargetId = ref<string | null>(null);
const pointDropPosition = ref<"before" | "after">("before");
// 列表点击"前/后插入"后,记录待插入位置,等地图点选坐标后写入该位置
const pendingInsertIndex = ref<number | null>(null);
const pendingInsertLabel = ref("");
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);
@@ -1473,7 +1479,8 @@ async function openConfigDialog(device: DeviceRecord) {
deviceConfigs[device.id] = config; deviceConfigs[device.id] = config;
configuringDeviceId.value = device.id; configuringDeviceId.value = device.id;
configDialogVisible.value = true; configDialogVisible.value = true;
mapEditorMode.value = "normal"; mapEditorMode.value = "select";
cancelInsertPoint();
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 ?? "";
@@ -1515,6 +1522,7 @@ function closeConfigDialog() {
configDialogVisible.value = false; configDialogVisible.value = false;
planDialogVisible.value = false; planDialogVisible.value = false;
configuringDeviceId.value = null; configuringDeviceId.value = null;
cancelInsertPoint();
clearSelectionBox(); clearSelectionBox();
clearDragPointState(); clearDragPointState();
clearMapPanState(); clearMapPanState();
@@ -2496,12 +2504,14 @@ function buildAutoPointName(type: PatrolPointType, index: number) {
return `${type} ${String(index + 1).padStart(2, "0")}`; return `${type} ${String(index + 1).padStart(2, "0")}`;
} }
function buildPointName(mode: EditorMode, type: PatrolPointType) { function buildPointName(mode: EditorMode, type: PatrolPointType, index?: number) {
const targetIndex = index ?? activeRoundPoints.value.length;
if (mode === "waypoint") { if (mode === "waypoint") {
return buildAutoPointName("途经点", activeRoundPoints.value.length); return buildAutoPointName("途经点", targetIndex);
} }
return buildAutoPointName(type, activeRoundPoints.value.length); return buildAutoPointName(type, targetIndex);
} }
function buildNextPointId() { function buildNextPointId() {
@@ -2723,10 +2733,13 @@ function handleMapStageClick(event: MouseEvent) {
} }
const type = buildPointTypeFromMode(mapEditorMode.value); const type = buildPointTypeFromMode(mapEditorMode.value);
const insertIndex = pendingInsertIndex.value;
const canInsert =
insertIndex !== null && insertIndex >= 0 && insertIndex <= currentConfig.value.points.length;
const point: PatrolPoint = { const point: PatrolPoint = {
id: buildNextPointId(), id: buildNextPointId(),
planId: currentConfig.value.plans[0]?.id ?? currentConfig.value.activePlanId, planId: currentConfig.value.plans[0]?.id ?? currentConfig.value.activePlanId,
name: buildPointName(mapEditorMode.value, type), name: buildPointName(mapEditorMode.value, type, insertIndex ?? undefined),
type, type,
direction: type === "业务点" ? "正向" : "正向", direction: type === "业务点" ? "正向" : "正向",
x: position.x, x: position.x,
@@ -2743,7 +2756,14 @@ function handleMapStageClick(event: MouseEvent) {
note: "新建点位,待补充巡检说明", note: "新建点位,待补充巡检说明",
}; };
currentConfig.value.points.push(point); if (canInsert && insertIndex !== null) {
currentConfig.value.points.splice(insertIndex, 0, point);
cancelInsertPoint();
mapEditorMode.value = "select";
} else {
currentConfig.value.points.push(point);
}
updateSelection([point.id]); updateSelection([point.id]);
} }
@@ -2930,6 +2950,86 @@ function removePoint(pointId: string) {
updateSelection(nextPoint ? [nextPoint.id] : []); updateSelection(nextPoint ? [nextPoint.id] : []);
} }
// 共享巡检点顺序即巡检顺序,可通过列表拖拽调整
function handlePointDragStart(point: PatrolPoint, event: DragEvent) {
cancelInsertPoint();
draggingPointId.value = point.id;
pointDropTargetId.value = null;
pointDropPosition.value = "before";
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", point.id);
}
}
function handlePointDragOver(point: PatrolPoint, event: DragEvent) {
if (!draggingPointId.value || draggingPointId.value === point.id) {
return;
}
event.preventDefault();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = "move";
}
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
pointDropTargetId.value = point.id;
pointDropPosition.value = event.clientY > rect.top + rect.height / 2 ? "after" : "before";
}
function handlePointDrop(point: PatrolPoint) {
const sourceId = draggingPointId.value;
const config = currentConfig.value;
if (!sourceId || !config || sourceId === point.id) {
resetPointDragState();
return;
}
const fromIndex = config.points.findIndex((item) => item.id === sourceId);
if (fromIndex < 0 || !config.points.some((item) => item.id === point.id)) {
resetPointDragState();
return;
}
const [movedPoint] = config.points.splice(fromIndex, 1);
const targetIndex = config.points.findIndex((item) => item.id === point.id);
const insertIndex = pointDropPosition.value === "after" ? targetIndex + 1 : targetIndex;
config.points.splice(insertIndex, 0, movedPoint);
resetPointDragState();
nextTick(() => {
scheduleMapCanvasRender();
});
}
function resetPointDragState() {
draggingPointId.value = null;
pointDropTargetId.value = null;
pointDropPosition.value = "before";
}
// 在指定点位前/后插入新点位:记录插入位置,随后在地图上点选坐标
function startInsertPoint(anchorIndex: number, position: "before" | "after") {
if (!currentConfig.value?.mapLoaded) {
return;
}
pendingInsertIndex.value = position === "before" ? anchorIndex : anchorIndex + 1;
pendingInsertLabel.value = `点位 ${anchorIndex + 1} ${position === "before" ? "之前" : "之后"}`;
mapEditorMode.value = "normal";
}
function cancelInsertPoint() {
pendingInsertIndex.value = null;
pendingInsertLabel.value = "";
}
function loadPlan(point: PatrolPoint) { function loadPlan(point: PatrolPoint) {
if (!activeRound.value) { if (!activeRound.value) {
return; return;
@@ -3359,6 +3459,13 @@ watch(
</div> </div>
<div class="editor-toolbar"> <div class="editor-toolbar">
<button class="editor-tool-btn icon" :class="{ active: mapEditorMode === 'select' }" type="button"
title="拖动查看" aria-label="拖动查看" @click="cancelInsertPoint(); mapEditorMode = 'select'">
<svg viewBox="0 0 24 24" aria-hidden="true">
<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" />
</svg>
</button>
<button v-for="mode in editorModes" :key="mode.id" :class="[ <button v-for="mode in editorModes" :key="mode.id" :class="[
'editor-tool-btn', 'editor-tool-btn',
{ active: mapEditorMode === mode.id }, { active: mapEditorMode === mode.id },
@@ -3368,6 +3475,10 @@ watch(
<button class="editor-tool-btn" type="button" @click="clearCanvas"> <button class="editor-tool-btn" type="button" @click="clearCanvas">
清空巡检点 清空巡检点
</button> </button>
<div v-if="pendingInsertIndex !== null" class="editor-tip">
在地图上点击放置新点位,将插入到{{ pendingInsertLabel }}
<button class="editor-tip-cancel" type="button" @click="cancelInsertPoint">取消插入</button>
</div>
</div> </div>
<div class="config-map-stage" ref="mapStageRef"> <div class="config-map-stage" ref="mapStageRef">
@@ -3442,14 +3553,28 @@ watch(
</div> </div>
<div class="rounds-point-column"> <div class="rounds-point-column">
<div class="sub-panel-title">共享巡检点</div> <div class="sub-panel-header">
<div class="sub-panel-title">共享巡检点</div>
<span class="sub-panel-hint">拖拽调整巡检顺序</span>
</div>
<div class="point-list-shell"> <div class="point-list-shell">
<div v-for="(point, index) in activeRoundPoints" :key="point.id" :class="[ <div v-for="(point, index) in activeRoundPoints" :key="point.id" :class="[
'point-list-item', 'point-list-item',
{ {
active: currentConfig.selectedPointIds.includes(point.id), active: currentConfig.selectedPointIds.includes(point.id),
dragging: draggingPointId === point.id,
'drop-before': pointDropTargetId === point.id && pointDropPosition === 'before',
'drop-after': pointDropTargetId === point.id && pointDropPosition === 'after',
}, },
]" @click="editPoint(point.id)"> ]" draggable="true" @click="editPoint(point.id)" @dragstart="handlePointDragStart(point, $event)"
@dragover="handlePointDragOver(point, $event)" @drop.prevent="handlePointDrop(point)"
@dragend="resetPointDragState">
<span class="point-drag-handle" title="拖拽调整顺序" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path
d="M8 6h2v2H8V6zm6 0h2v2h-2V6zM8 11h2v2H8v-2zm6 0h2v2h-2v-2zM8 16h2v2H8v-2zm6 0h2v2h-2v-2z" />
</svg>
</span>
<div class="point-list-main"> <div class="point-list-main">
<div class="point-list-title">点位 {{ index + 1 }}</div> <div class="point-list-title">点位 {{ index + 1 }}</div>
<div class="point-list-meta"> <div class="point-list-meta">
@@ -3460,6 +3585,18 @@ watch(
</div> </div>
</div> </div>
<div class="point-list-actions"> <div class="point-list-actions">
<button class="icon" type="button" :title="`在点位 ${index + 1} 之前插入新点位`"
aria-label="在此点位前插入" @click.stop="startInsertPoint(index, 'before')">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M11 18h2V8.83l4.59 4.58L19 12l-7-7-7 7 1.41 1.41L11 8.83V18z" />
</svg>
</button>
<button class="icon" type="button" :title="`在点位 ${index + 1} 之后插入新点位`"
aria-label="在此点位后插入" @click.stop="startInsertPoint(index, 'after')">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M13 6h-2v9.17l-4.59-4.58L5 12l7 7 7-7-1.41-1.41L13 15.17V6z" />
</svg>
</button>
<button type="button" @click.stop="openPlanDialog(point)">编辑</button> <button type="button" @click.stop="openPlanDialog(point)">编辑</button>
<button class="danger" type="button" @click.stop="removePoint(point.id)"> <button class="danger" type="button" @click.stop="removePoint(point.id)">
删除 删除
@@ -4015,6 +4152,20 @@ watch(
cursor: pointer; cursor: pointer;
} }
.editor-tool-btn.icon {
width: 34px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
.editor-tool-btn.icon svg {
width: 18px;
height: 18px;
fill: currentColor;
}
.editor-tool-btn.active { .editor-tool-btn.active {
color: #eefcff; color: #eefcff;
border-color: rgba(102, 235, 255, 0.7); border-color: rgba(102, 235, 255, 0.7);
@@ -4025,10 +4176,23 @@ watch(
.editor-tip { .editor-tip {
margin-left: auto; margin-left: auto;
color: rgba(161, 218, 243, 0.72); display: inline-flex;
align-items: center;
gap: 8px;
color: rgba(255, 214, 130, 0.92);
font-size: 12px; font-size: 12px;
} }
.editor-tip-cancel {
padding: 0;
border: 0;
background: transparent;
color: #8fd6ff;
font-size: 12px;
cursor: pointer;
text-decoration: underline;
}
.map-meta-strip { .map-meta-strip {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -4753,9 +4917,58 @@ watch(
box-shadow: inset 0 0 0 1px rgba(255, 196, 118, 0.12); box-shadow: inset 0 0 0 1px rgba(255, 196, 118, 0.12);
} }
.sub-panel-hint {
color: rgba(161, 218, 243, 0.72);
font-size: 11px;
}
.point-drag-handle {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
color: rgba(140, 205, 235, 0.6);
cursor: grab;
}
.point-drag-handle svg {
width: 16px;
height: 16px;
fill: currentColor;
}
.point-list-item.dragging {
opacity: 0.45;
}
.point-list-item.drop-before {
box-shadow: inset 0 2px 0 0 rgba(102, 235, 255, 0.85);
}
.point-list-item.drop-after {
box-shadow: inset 0 -2px 0 0 rgba(102, 235, 255, 0.85);
}
.point-list-actions { .point-list-actions {
display: flex; display: flex;
gap: 8px; align-items: center;
gap: 6px;
}
.point-list-actions button.icon {
width: 28px;
min-width: 28px;
height: 30px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
.point-list-actions button.icon svg {
width: 14px;
height: 14px;
fill: currentColor;
} }
.point-list-actions .danger, .point-list-actions .danger,

View File

@@ -31,12 +31,12 @@ export default defineConfig(({ command }: ConfigEnv) => {
host: true, host: true,
proxy: { proxy: {
'/api': { '/api': {
target: `http://192.168.3.233:16003/`, target: `http://192.168.3.110:16003/`,
changeOrigin: true, changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/api/, '') rewrite: (path: string) => path.replace(/^\/api/, '')
}, },
'/ws': { '/ws': {
target: `ws://192.168.3.233:16003/`, target: `ws://192.168.3.110:16003/`,
changeOrigin: true, changeOrigin: true,
ws: true ws: true
} }