优化标点交互
This commit is contained in:
@@ -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: "新建点位,待补充巡检说明",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (canInsert && insertIndex !== null) {
|
||||||
|
currentConfig.value.points.splice(insertIndex, 0, point);
|
||||||
|
cancelInsertPoint();
|
||||||
|
mapEditorMode.value = "select";
|
||||||
|
} else {
|
||||||
currentConfig.value.points.push(point);
|
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-header">
|
||||||
<div class="sub-panel-title">共享巡检点</div>
|
<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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user