feat: canvas drag/resize/selection, modal planner, default OneWay (唐超)
- Canvas: 坐标轴、顶点坐标、框拖拽、角点缩放、点击选中高亮 - 路径规划改为弹窗覆盖层,编辑状态不丢失 - 展开任务状态持久化到 Pinia store - 扫描模式默认 OneWay - 保存时 startTime/endTime/durationMinutes 清空为 "" - exposureTime/frameRate/captureIntervalSeconds 默认 0 - 去除生成航线后的扫描线文字标注
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "happa-mission-plan",
|
||||
"private": true,
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@ -3180,7 +3180,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "spectral-insight-mission-plan"
|
||||
version = "0.0.4"
|
||||
version = "0.0.5"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"chrono",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "spectral-insight-mission-plan"
|
||||
version = "0.0.5"
|
||||
version = "0.0.6"
|
||||
description = "Spectral Insight Mission Plan"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
@ -3,7 +3,27 @@ use crate::error::AppError;
|
||||
use crate::models::MissionPlan;
|
||||
|
||||
pub fn write_mission(path: &Path, mission: &MissionPlan) -> Result<(), AppError> {
|
||||
let content = serde_json::to_string_pretty(mission)?;
|
||||
// Clear computed timing fields before saving
|
||||
let mut cleaned = mission.clone();
|
||||
for task in &mut cleaned.tasks {
|
||||
task.duration_minutes = 0.0;
|
||||
task.estimated_duration_minutes = 0.0;
|
||||
task.start_time = Some("".to_string());
|
||||
task.end_time = Some("".to_string());
|
||||
for sub in &mut task.sub_tasks {
|
||||
sub.duration_minutes = 0.0;
|
||||
sub.estimated_duration_minutes = 0.0;
|
||||
sub.start_time = Some("".to_string());
|
||||
sub.end_time = Some("".to_string());
|
||||
if sub.capture_interval_seconds.is_none() { sub.capture_interval_seconds = Some(0); }
|
||||
if sub.exposure_time.is_none() { sub.exposure_time = Some(0); }
|
||||
if sub.frame_rate.is_none() { sub.frame_rate = Some(0); }
|
||||
}
|
||||
}
|
||||
let content = serde_json::to_string_pretty(&cleaned)?;
|
||||
let content = content
|
||||
.replace("\"durationMinutes\": 0.0", "\"durationMinutes\": \"\"")
|
||||
.replace("\"estimatedDurationMinutes\": 0.0", "\"estimatedDurationMinutes\": \"\"");
|
||||
std::fs::write(path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -10,8 +10,12 @@ pub struct ScanRegion {
|
||||
pub y_min: f64,
|
||||
#[serde(rename = "yMax")]
|
||||
pub y_max: f64,
|
||||
#[serde(default = "default_shape")]
|
||||
pub shape: String,
|
||||
}
|
||||
|
||||
fn default_shape() -> String { "Rect".to_string() }
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CameraParams {
|
||||
#[serde(rename = "fovDegrees")]
|
||||
@ -67,7 +71,7 @@ impl Default for PlannerDefaults {
|
||||
speed_y_cm_s: 5.0,
|
||||
speed_x_start_cm_s: 20.0,
|
||||
speed_x_scan_cm_s: 10.0,
|
||||
mode: "Zigzag".to_string(),
|
||||
mode: "OneWay".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,10 +68,10 @@ impl SubTask {
|
||||
Self {
|
||||
id: default_subtask_id(),
|
||||
sub_task_type,
|
||||
capture_interval_seconds: None,
|
||||
capture_interval_seconds: Some(0),
|
||||
default_render_band: Some(550),
|
||||
exposure_time: None,
|
||||
frame_rate: None,
|
||||
exposure_time: Some(0),
|
||||
frame_rate: Some(0),
|
||||
path_line_file_path: String::new(),
|
||||
duration_minutes: 0.0,
|
||||
estimated_duration_minutes: 0.0,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Spectral Insight Mission Plan",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.6",
|
||||
"identifier": "com.spectral-insight.mission-plan",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@ -1,18 +1,8 @@
|
||||
<template>
|
||||
<div ref="canvasContainer" class="scan-canvas">
|
||||
<canvas
|
||||
ref="canvas"
|
||||
@mousedown="onMouseDown"
|
||||
@mousemove="onMouseMove"
|
||||
@mouseup="onMouseUp"
|
||||
@mouseleave="onMouseUp"
|
||||
/>
|
||||
<div class="canvas-info" v-if="regions.length > 0">
|
||||
{{ regions.length }} 个框 | 点击「画框」开始绘制新框
|
||||
</div>
|
||||
<div class="canvas-info" v-else>
|
||||
点击「画框」在画布上绘制扫描区域
|
||||
</div>
|
||||
<canvas ref="canvas" @mousedown="onMouseDown" @mousemove="onMouseMove" @mouseup="onMouseUp" @mouseleave="onMouseUp" />
|
||||
<div class="canvas-info" v-if="regions.length > 0">{{ regions.length }} 个框 | 点击选中 | 拖动移动/缩放</div>
|
||||
<div class="canvas-info" v-else>点击「矩形」在画布上绘制扫描区域</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -22,210 +12,112 @@ import type { ScanRegion, ScanMode, ScanShape } from '../../types/path-plan';
|
||||
import type { PathLineRecord } from '../../types/path-line';
|
||||
|
||||
const props = defineProps<{
|
||||
regions: ScanRegion[];
|
||||
records: PathLineRecord[];
|
||||
mode: ScanMode;
|
||||
backgroundImage?: string;
|
||||
drawingMode: boolean;
|
||||
drawingShape?: ScanShape;
|
||||
records: PathLineRecord[]; mode: ScanMode; backgroundImage?: string;
|
||||
drawingMode: boolean; drawingShape?: ScanShape; selectedIndex?: number;
|
||||
}>();
|
||||
const regions = defineModel<ScanRegion[]>('regions', { required: true });
|
||||
const emit = defineEmits<{ 'add-region': [region: ScanRegion]; 'select-region': [index: number]; 'deselect': [] }>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'add-region': [region: ScanRegion];
|
||||
}>();
|
||||
|
||||
const canvasContainer = ref<HTMLDivElement>();
|
||||
const canvas = ref<HTMLCanvasElement>();
|
||||
const isDrawing = ref(false);
|
||||
const dragStart = ref({ x: 0, y: 0 });
|
||||
const drawingRect = ref<ScanRegion | null>(null);
|
||||
const bgImage = ref<HTMLImageElement | null>(null);
|
||||
const canvasContainer = ref<HTMLDivElement>(); const canvas = ref<HTMLCanvasElement>();
|
||||
const isDrawing = ref(false); const isDragging = ref(false); const isResizing = ref(false);
|
||||
const resizeCorner = ref(''); const dragTarget = ref(-1);
|
||||
const dragStart = ref({ x: 0, y: 0 }); const regionStart = ref<ScanRegion | null>(null);
|
||||
const drawingRect = ref<ScanRegion | null>(null); const bgImage = ref<HTMLImageElement | null>(null);
|
||||
const padding = 40;
|
||||
|
||||
function getCanvasSize() {
|
||||
const rect = canvasContainer.value?.getBoundingClientRect();
|
||||
return { w: rect?.width || 600, h: rect?.height || 400 };
|
||||
}
|
||||
|
||||
function toCanvas(clientX: number, clientY: number) {
|
||||
const rect = canvas.value!.getBoundingClientRect();
|
||||
return { x: clientX - rect.left, y: clientY - rect.top };
|
||||
}
|
||||
|
||||
function toWorld(cx: number, cy: number) {
|
||||
const { w, h } = getCanvasSize();
|
||||
const drawW = w - padding * 2;
|
||||
const drawH = h - padding * 2;
|
||||
const worldX = (cx - padding) / drawW * 100;
|
||||
const worldY = (cy - padding) / drawH * 100;
|
||||
return { x: Math.max(0, Math.min(100, worldX)), y: Math.max(0, Math.min(100, 100 - worldY)) };
|
||||
}
|
||||
|
||||
async function loadBackgroundImage(path: string | undefined) {
|
||||
if (!path) { bgImage.value = null; draw(); return; }
|
||||
try {
|
||||
const { convertFileSrc } = await import('@tauri-apps/api/core');
|
||||
const assetUrl = convertFileSrc(path);
|
||||
const img = new Image();
|
||||
img.onload = () => { bgImage.value = img; draw(); };
|
||||
img.onerror = () => { bgImage.value = null; draw(); };
|
||||
img.src = assetUrl;
|
||||
} catch { bgImage.value = null; draw(); }
|
||||
}
|
||||
|
||||
function worldToCanvas(worldX: number, worldY: number) {
|
||||
const { w, h } = getCanvasSize();
|
||||
const drawW = w - padding * 2;
|
||||
const drawH = h - padding * 2;
|
||||
const x = padding + (worldX / 100) * drawW;
|
||||
const y = padding + ((100 - worldY) / 100) * drawH;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
const COLORS = ['#2080f0', '#f0a020', '#18a058', '#d03050', '#a060e0', '#e06080'];
|
||||
|
||||
function gs() { const r = canvasContainer.value?.getBoundingClientRect(); return { w: r?.width || 600, h: r?.height || 400 }; }
|
||||
function tc(cx: number, cy: number) { const r = canvas.value!.getBoundingClientRect(); return { x: cx - r.left, y: cy - r.top }; }
|
||||
function tw(cx: number, cy: number) { const { w, h } = gs(); const dw = w - padding * 2, dh = h - padding * 2; return { x: Math.max(0, Math.min(100, (cx - padding) / dw * 100)), y: Math.max(0, Math.min(100, 100 - (cy - padding) / dh * 100)) }; }
|
||||
function w2c(wx: number, wy: number) { const { w, h } = gs(); const dw = w - padding * 2, dh = h - padding * 2; return { x: padding + (wx / 100) * dw, y: padding + ((100 - wy) / 100) * dh }; }
|
||||
function findR(p: { x: number; y: number }): number { const r = regions.value; for (let i = r.length - 1; i >= 0; i--) { if (p.x >= r[i].xMin && p.x <= r[i].xMax && p.y >= r[i].yMin && p.y <= r[i].yMax) return i; } return -1; }
|
||||
function findC(p: { x: number; y: number }): { i: number; c: string } | null { const th = 3; for (let i = 0; i < regions.value.length; i++) { const r = regions.value[i]; for (const [c, cx, cy] of [['tl', r.xMin, r.yMax] as const, ['tr', r.xMax, r.yMax] as const, ['bl', r.xMin, r.yMin] as const, ['br', r.xMax, r.yMin] as const]) { if (Math.abs(p.x - cx) < th && Math.abs(p.y - cy) < th) return { i, c }; } } return null; }
|
||||
|
||||
async function loadBG(p: string | undefined) {
|
||||
if (!p) { bgImage.value = null; draw(); return; }
|
||||
try { const { convertFileSrc } = await import('@tauri-apps/api/core'); const img = new Image(); img.onload = () => { bgImage.value = img; draw(); }; img.onerror = () => { bgImage.value = null; draw(); }; img.src = convertFileSrc(p); } catch { bgImage.value = null; draw(); }
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const ctx = canvas.value?.getContext('2d');
|
||||
if (!ctx) return;
|
||||
const ctx = canvas.value?.getContext('2d'); if (!ctx) return;
|
||||
const { w, h } = gs(); const dpr = window.devicePixelRatio || 1;
|
||||
canvas.value!.width = w * dpr; canvas.value!.height = h * dpr;
|
||||
canvas.value!.style.width = w + 'px'; canvas.value!.style.height = h + 'px';
|
||||
ctx.scale(dpr, dpr); ctx.fillStyle = '#f8f8f8'; ctx.fillRect(0, 0, w, h);
|
||||
if (bgImage.value) { ctx.globalAlpha = 0.5; ctx.drawImage(bgImage.value, 0, 0, w, h); ctx.globalAlpha = 1.0; }
|
||||
const dw = w - padding * 2, dh = h - padding * 2, ax = 16;
|
||||
|
||||
const { w, h } = getCanvasSize();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.value!.width = w * dpr;
|
||||
canvas.value!.height = h * dpr;
|
||||
canvas.value!.style.width = w + 'px';
|
||||
canvas.value!.style.height = h + 'px';
|
||||
ctx.scale(dpr, dpr);
|
||||
ctx.strokeStyle = '#e8e8e8'; ctx.lineWidth = 1;
|
||||
for (let i = 0; i <= 10; i++) { const x = padding + (i / 10) * dw; ctx.beginPath(); ctx.moveTo(x, padding); ctx.lineTo(x, padding + dh); ctx.stroke(); }
|
||||
for (let i = 0; i <= 10; i++) { const y = padding + (i / 10) * dh; ctx.beginPath(); ctx.moveTo(padding, y); ctx.lineTo(padding + dw, y); ctx.stroke(); }
|
||||
ctx.strokeStyle = '#666'; ctx.lineWidth = 1.5;
|
||||
ctx.beginPath(); ctx.moveTo(padding - ax, padding + dh); ctx.lineTo(padding + dw + ax, padding + dh); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(padding, padding + dh + ax); ctx.lineTo(padding, padding - ax); ctx.stroke();
|
||||
ctx.fillStyle = '#888'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center';
|
||||
for (let i = 0; i <= 10; i++) { const x = padding + (i / 10) * dw; ctx.fillText(String(i * 10), x, padding + dh + ax + 12); ctx.beginPath(); ctx.moveTo(x, padding + dh + ax - 3); ctx.lineTo(x, padding + dh + ax + 3); ctx.stroke(); }
|
||||
ctx.textAlign = 'right';
|
||||
for (let i = 0; i <= 10; i++) { const y = padding + (i / 10) * dh; ctx.fillText(String((10 - i) * 10), padding - ax - 4, y + 4); ctx.beginPath(); ctx.moveTo(padding - ax - 3, y); ctx.lineTo(padding - ax + 3, y); ctx.stroke(); }
|
||||
ctx.textAlign = 'start'; ctx.font = '11px sans-serif';
|
||||
ctx.fillText('X (cm)', padding + dw + ax - 10, padding + dh + ax + 24);
|
||||
ctx.save(); ctx.translate(padding - ax - 24, 10); ctx.rotate(-Math.PI / 2); ctx.fillText('Y (cm)', 0, 0); ctx.restore();
|
||||
ctx.strokeStyle = '#f0a020'; ctx.lineWidth = 1; ctx.setLineDash([4, 4]); const z1 = w2c(0, 0), z2 = w2c(100, 100); ctx.strokeRect(z1.x, z1.y, z2.x - z1.x, z2.y - z1.y); ctx.setLineDash([]);
|
||||
|
||||
ctx.fillStyle = '#f8f8f8';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
if (bgImage.value) {
|
||||
ctx.globalAlpha = 0.5;
|
||||
ctx.drawImage(bgImage.value, 0, 0, w, h);
|
||||
ctx.globalAlpha = 1.0;
|
||||
for (let i = 0; i < regions.value.length; i++) {
|
||||
const r = regions.value[i]; const p1 = w2c(r.xMin, r.yMin), p2 = w2c(r.xMax, r.yMax);
|
||||
const c = COLORS[i % COLORS.length]; const sel = i === props.selectedIndex;
|
||||
ctx.strokeStyle = sel ? '#ff0000' : c; ctx.lineWidth = sel ? 3 : 2;
|
||||
if (r.shape === 'Circle') { ctx.beginPath(); ctx.ellipse((p1.x + p2.x) / 2, (p1.y + p2.y) / 2, Math.abs(p2.x - p1.x) / 2, Math.abs(p2.y - p1.y) / 2, 0, 0, Math.PI * 2); ctx.stroke(); }
|
||||
else { ctx.strokeRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y); ctx.fillStyle = '#fff'; for (const [cx, cy] of [[p1.x, p1.y], [p2.x, p1.y], [p1.x, p2.y], [p2.x, p2.y]]) { ctx.fillRect(cx - 4, cy - 4, 8, 8); ctx.strokeRect(cx - 4, cy - 4, 8, 8); } }
|
||||
ctx.fillStyle = c; ctx.font = 'bold 12px sans-serif'; ctx.fillText(`#${i + 1}`, p1.x + 4, p1.y - 4);
|
||||
ctx.font = '10px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(`(${r.xMin.toFixed(1)}, ${r.yMin.toFixed(1)})`, p1.x + 4, p2.y + 14);
|
||||
ctx.textAlign = 'right'; ctx.fillText(`(${r.xMax.toFixed(1)}, ${r.yMax.toFixed(1)})`, p2.x - 4, p1.y - 8); ctx.textAlign = 'start';
|
||||
}
|
||||
|
||||
const drawW = w - padding * 2;
|
||||
const drawH = h - padding * 2;
|
||||
|
||||
// Grid
|
||||
ctx.strokeStyle = '#e8e8e8';
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
const x = padding + (i / 10) * drawW;
|
||||
ctx.beginPath(); ctx.moveTo(x, padding); ctx.lineTo(x, padding + drawH); ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw all regions (rect or circle)
|
||||
for (let i = 0; i < props.regions.length; i++) {
|
||||
const r = props.regions[i];
|
||||
const p1 = worldToCanvas(r.xMin, r.yMin);
|
||||
const p2 = worldToCanvas(r.xMax, r.yMax);
|
||||
ctx.strokeStyle = COLORS[i % COLORS.length];
|
||||
ctx.lineWidth = 2;
|
||||
if (r.shape === 'Circle') {
|
||||
const cx = (p1.x + p2.x) / 2;
|
||||
const cy = (p1.y + p2.y) / 2;
|
||||
const rx = Math.abs(p2.x - p1.x) / 2;
|
||||
const ry = Math.abs(p2.y - p1.y) / 2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
} else {
|
||||
ctx.strokeRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y);
|
||||
}
|
||||
// Label
|
||||
ctx.fillStyle = COLORS[i % COLORS.length];
|
||||
ctx.font = 'bold 12px sans-serif';
|
||||
ctx.fillText(`#${i + 1}`, p1.x + 4, p1.y - 4);
|
||||
}
|
||||
|
||||
// Draw current drawing shape
|
||||
if (drawingRect.value) {
|
||||
const p1 = worldToCanvas(drawingRect.value.xMin, drawingRect.value.yMin);
|
||||
const p2 = worldToCanvas(drawingRect.value.xMax, drawingRect.value.yMax);
|
||||
ctx.strokeStyle = '#f00';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([5, 5]);
|
||||
if (props.drawingShape === 'Circle') {
|
||||
const cx = (p1.x + p2.x) / 2;
|
||||
const cy = (p1.y + p2.y) / 2;
|
||||
const rx = Math.abs(p2.x - p1.x) / 2;
|
||||
const ry = Math.abs(p2.y - p1.y) / 2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
} else {
|
||||
ctx.strokeRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y);
|
||||
}
|
||||
ctx.setLineDash([]);
|
||||
const p1 = w2c(drawingRect.value.xMin, drawingRect.value.yMin), p2 = w2c(drawingRect.value.xMax, drawingRect.value.yMax);
|
||||
ctx.strokeStyle = '#f00'; ctx.lineWidth = 2; ctx.setLineDash([5, 5]);
|
||||
if (props.drawingShape === 'Circle') { ctx.beginPath(); ctx.ellipse((p1.x + p2.x) / 2, (p1.y + p2.y) / 2, Math.abs(p2.x - p1.x) / 2, Math.abs(p2.y - p1.y) / 2, 0, 0, Math.PI * 2); ctx.stroke(); }
|
||||
else { ctx.strokeRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y); }
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
// Draw scan paths
|
||||
if (props.records.length > 0) {
|
||||
for (const record of props.records) {
|
||||
const start = worldToCanvas(record.targetXMinPosition, record.targetYPosition);
|
||||
const end = worldToCanvas(record.targetXMaxPosition, record.targetYPosition);
|
||||
ctx.beginPath(); ctx.moveTo(start.x, start.y); ctx.lineTo(end.x, end.y);
|
||||
ctx.strokeStyle = '#18a058'; ctx.lineWidth = 1.5; ctx.stroke();
|
||||
const angle = Math.atan2(end.y - start.y, end.x - start.x);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(end.x, end.y);
|
||||
ctx.lineTo(end.x - 8 * Math.cos(angle - 0.4), end.y - 8 * Math.sin(angle - 0.4));
|
||||
ctx.lineTo(end.x - 8 * Math.cos(angle + 0.4), end.y - 8 * Math.sin(angle + 0.4));
|
||||
ctx.closePath(); ctx.fillStyle = '#18a058'; ctx.fill();
|
||||
}
|
||||
for (const r of props.records) { const s = w2c(r.targetXMinPosition, r.targetYPosition), e = w2c(r.targetXMaxPosition, r.targetYPosition); ctx.beginPath(); ctx.moveTo(s.x, s.y); ctx.lineTo(e.x, e.y); ctx.strokeStyle = '#18a058'; ctx.lineWidth = 1.5; ctx.stroke(); const a = Math.atan2(e.y - s.y, e.x - s.x); ctx.beginPath(); ctx.moveTo(e.x, e.y); ctx.lineTo(e.x - 6 * Math.cos(a - 0.4), e.y - 6 * Math.sin(a - 0.4)); ctx.lineTo(e.x - 6 * Math.cos(a + 0.4), e.y - 6 * Math.sin(a + 0.4)); ctx.closePath(); ctx.fillStyle = '#18a058'; ctx.fill(); }
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
if (!props.drawingMode) return;
|
||||
isDrawing.value = true;
|
||||
dragStart.value = toCanvas(e.clientX, e.clientY);
|
||||
const cp = tc(e.clientX, e.clientY);
|
||||
if (props.drawingMode) { isDrawing.value = true; dragStart.value = cp; return; }
|
||||
const wp = tw(cp.x, cp.y);
|
||||
const corner = findC(wp);
|
||||
if (corner && regions.value[corner.i].shape !== 'Circle') {
|
||||
isResizing.value = true; resizeCorner.value = corner.c; dragTarget.value = corner.i;
|
||||
regionStart.value = { ...regions.value[corner.i] }; dragStart.value = cp; return;
|
||||
}
|
||||
const hit = findR(wp);
|
||||
if (hit >= 0) { isDragging.value = true; dragTarget.value = hit; regionStart.value = { ...regions.value[hit] }; dragStart.value = cp; emit('select-region', hit); return; }
|
||||
// Clicked empty space
|
||||
if (!props.drawingMode) { emit('deselect'); }
|
||||
}
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
if (!isDrawing.value || !props.drawingMode) return;
|
||||
const p = toCanvas(e.clientX, e.clientY);
|
||||
const p1 = toWorld(dragStart.value.x, dragStart.value.y);
|
||||
const p2 = toWorld(p.x, p.y);
|
||||
drawingRect.value = {
|
||||
shape: props.drawingShape ? props.drawingShape : "Rect",
|
||||
xMin: Math.min(p1.x, p2.x), xMax: Math.max(p1.x, p2.x),
|
||||
yMin: Math.min(p1.y, p2.y), yMax: Math.max(p1.y, p2.y),
|
||||
};
|
||||
draw();
|
||||
const cp = tc(e.clientX, e.clientY);
|
||||
if (isDrawing.value && props.drawingMode) { const p1 = tw(dragStart.value.x, dragStart.value.y), p2 = tw(cp.x, cp.y); drawingRect.value = { shape: props.drawingShape || 'Rect', xMin: Math.min(p1.x, p2.x), xMax: Math.max(p1.x, p2.x), yMin: Math.min(p1.y, p2.y), yMax: Math.max(p1.y, p2.y) }; draw(); return; }
|
||||
if (isDragging.value && dragTarget.value >= 0 && regionStart.value) { const dx = (cp.x - dragStart.value.x) / (gs().w - padding * 2) * 100, dy = -(cp.y - dragStart.value.y) / (gs().h - padding * 2) * 100; const rs = regionStart.value, rw = rs.xMax - rs.xMin, rh = rs.yMax - rs.yMin; const arr = [...regions.value]; arr[dragTarget.value] = { ...arr[dragTarget.value], xMin: Math.max(0, Math.min(100 - rw, rs.xMin + dx)), xMax: Math.max(rs.xMin + dx + 1, Math.min(100, rs.xMax + dx)), yMin: Math.max(0, Math.min(100 - rh, rs.yMin + dy)), yMax: Math.max(rs.yMin + dy + 1, Math.min(100, rs.yMax + dy)) }; regions.value = arr; draw(); return; }
|
||||
if (isResizing.value && dragTarget.value >= 0 && regionStart.value) { const p1 = tw(dragStart.value.x, dragStart.value.y), p2 = tw(cp.x, cp.y); const dx = p2.x - p1.x, dy = p2.y - p1.y, rs = regionStart.value; let xMin = rs.xMin, xMax = rs.xMax, yMin = rs.yMin, yMax = rs.yMax; const c = resizeCorner.value; if (c.includes('l')) xMin = Math.min(rs.xMin + dx, rs.xMax - 1); if (c.includes('r')) xMax = Math.max(rs.xMax + dx, rs.xMin + 1); if (c.includes('b')) yMin = Math.min(rs.yMin + dy, rs.yMax - 1); if (c.includes('t')) yMax = Math.max(rs.yMax + dy, rs.yMin + 1); xMin = Math.max(0, xMin); xMax = Math.min(100, xMax); yMin = Math.max(0, yMin); yMax = Math.min(100, yMax); if (xMax - xMin > 1 && yMax - yMin > 1) { const arr = [...regions.value]; arr[dragTarget.value] = { ...arr[dragTarget.value], xMin, xMax, yMin, yMax }; regions.value = arr; draw(); } }
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
if (!isDrawing.value) return;
|
||||
isDrawing.value = false;
|
||||
if (drawingRect.value) {
|
||||
const r = drawingRect.value;
|
||||
if (r.xMax - r.xMin > 1 && r.yMax - r.yMin > 1) {
|
||||
emit('add-region', { ...r, shape: props.drawingShape ? props.drawingShape : 'Rect' });
|
||||
}
|
||||
drawingRect.value = null;
|
||||
draw();
|
||||
}
|
||||
if (isDrawing.value) { isDrawing.value = false; if (drawingRect.value) { const r = drawingRect.value; if (r.xMax - r.xMin > 1 && r.yMax - r.yMin > 1) emit('add-region', { ...r, shape: props.drawingShape || 'Rect' }); drawingRect.value = null; draw(); } return; }
|
||||
if (isDragging.value || isResizing.value) { isDragging.value = false; isResizing.value = false; dragTarget.value = -1; regionStart.value = null; }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadBackgroundImage(props.backgroundImage);
|
||||
window.addEventListener('resize', draw);
|
||||
});
|
||||
|
||||
watch(() => [props.regions, props.records, props.mode, props.drawingMode, props.drawingShape], () => { draw(); });
|
||||
watch(() => props.backgroundImage, (v) => { loadBackgroundImage(v); });
|
||||
onMounted(() => { loadBG(props.backgroundImage); window.addEventListener('resize', draw); });
|
||||
watch(() => [regions.value, props.records, props.mode, props.drawingMode, props.drawingShape, props.selectedIndex], () => { draw(); });
|
||||
watch(() => props.backgroundImage, (v) => { loadBG(v); });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.scan-canvas { position: relative; width: 100%; height: 100%; }
|
||||
canvas { display: block; width: 100%; height: 100%; }
|
||||
.canvas-info {
|
||||
position: absolute; bottom: 8px; left: 8px;
|
||||
background: rgba(0,0,0,0.6); color: white;
|
||||
padding: 4px 8px; border-radius: 4px; font-size: 12px; pointer-events: none;
|
||||
}
|
||||
.canvas-info { position: absolute; bottom: 8px; left: 8px; background: rgba(0,0,0,0.6); color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; pointer-events: none; }
|
||||
</style>
|
||||
|
||||
@ -10,38 +10,20 @@ export const useMissionStore = defineStore('mission', () => {
|
||||
const mission = ref<MissionPlan>(createEmptyMission());
|
||||
const currentFilePath = ref<string | null>(null);
|
||||
const isDirty = ref(false);
|
||||
const expandedTaskId = ref<string | null>(null);
|
||||
|
||||
const taskCount = computed(() => mission.value.tasks.length);
|
||||
|
||||
// Auto-persist scan config and background when they change
|
||||
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
watch(
|
||||
() => ({ sc: mission.value.scanConfig, bg: mission.value.backgroundImage }),
|
||||
() => {
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
saveTimer = setTimeout(() => {
|
||||
invoke('save_default_scan_config', { config: mission.value.scanConfig }).catch(() => {});
|
||||
invoke('save_default_background', { path: mission.value.backgroundImage || '' }).catch(() => {});
|
||||
}, 500);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
async function createNew() {
|
||||
mission.value = createEmptyMission();
|
||||
// Load persisted defaults
|
||||
try {
|
||||
const defaults = await invoke<MissionScanConfig>('load_default_scan_config');
|
||||
mission.value.scanConfig = defaults;
|
||||
} catch {
|
||||
// use defaults
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
try {
|
||||
const bg = await invoke<string | null>('load_default_background');
|
||||
mission.value.backgroundImage = bg;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
currentFilePath.value = null;
|
||||
isDirty.value = false;
|
||||
}
|
||||
@ -80,40 +62,25 @@ export const useMissionStore = defineStore('mission', () => {
|
||||
}
|
||||
|
||||
async function addSubTask(taskId: number, subTaskType: SubTaskType) {
|
||||
const result = await invoke<MissionPlan>('add_sub_task', {
|
||||
mission: mission.value,
|
||||
taskId,
|
||||
subTaskType,
|
||||
});
|
||||
const result = await invoke<MissionPlan>('add_sub_task', { mission: mission.value, taskId, subTaskType });
|
||||
mission.value = result;
|
||||
isDirty.value = true;
|
||||
}
|
||||
|
||||
async function removeSubTask(taskId: number, subTaskId: string) {
|
||||
const result = await invoke<MissionPlan>('remove_sub_task', {
|
||||
mission: mission.value,
|
||||
taskId,
|
||||
subTaskId,
|
||||
});
|
||||
const result = await invoke<MissionPlan>('remove_sub_task', { mission: mission.value, taskId, subTaskId });
|
||||
mission.value = result;
|
||||
isDirty.value = true;
|
||||
}
|
||||
|
||||
async function updateSubTask(taskId: number, subTask: SubTask) {
|
||||
const result = await invoke<MissionPlan>('update_sub_task', {
|
||||
mission: mission.value,
|
||||
taskId,
|
||||
subTask,
|
||||
});
|
||||
const result = await invoke<MissionPlan>('update_sub_task', { mission: mission.value, taskId, subTask });
|
||||
mission.value = result;
|
||||
isDirty.value = true;
|
||||
}
|
||||
|
||||
async function updateTask(task: Task) {
|
||||
const result = await invoke<MissionPlan>('update_task', {
|
||||
mission: mission.value,
|
||||
task,
|
||||
});
|
||||
const result = await invoke<MissionPlan>('update_task', { mission: mission.value, task });
|
||||
mission.value = result;
|
||||
isDirty.value = true;
|
||||
}
|
||||
@ -138,24 +105,26 @@ export const useMissionStore = defineStore('mission', () => {
|
||||
isDirty.value = true;
|
||||
}
|
||||
|
||||
// Auto-persist scan config and background when they change
|
||||
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
watch(
|
||||
() => ({ sc: mission.value.scanConfig, bg: mission.value.backgroundImage }),
|
||||
() => {
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
saveTimer = setTimeout(() => {
|
||||
invoke('save_default_scan_config', { config: mission.value.scanConfig }).catch(() => {});
|
||||
invoke('save_default_background', { path: mission.value.backgroundImage || '' }).catch(() => {});
|
||||
}, 500);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
return {
|
||||
mission,
|
||||
currentFilePath,
|
||||
isDirty,
|
||||
taskCount,
|
||||
createNew,
|
||||
loadMission,
|
||||
saveMission,
|
||||
addTask,
|
||||
copyTask,
|
||||
removeTask,
|
||||
addSubTask,
|
||||
removeSubTask,
|
||||
updateSubTask,
|
||||
updateTask,
|
||||
calculateSchedule,
|
||||
runValidation,
|
||||
updateScanConfig,
|
||||
setBackgroundImage,
|
||||
mission, currentFilePath, isDirty, expandedTaskId, taskCount,
|
||||
createNew, loadMission, saveMission,
|
||||
addTask, copyTask, removeTask,
|
||||
addSubTask, removeSubTask, updateSubTask, updateTask,
|
||||
calculateSchedule, runValidation,
|
||||
updateScanConfig, setBackgroundImage,
|
||||
};
|
||||
});
|
||||
|
||||
@ -59,7 +59,7 @@
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
|
||||
<n-collapse v-if="missionStore.mission.tasks.length > 0" accordion>
|
||||
<n-collapse v-if="missionStore.mission.tasks.length > 0" accordion v-model:expanded-name="expandedTask">
|
||||
<n-collapse-item
|
||||
v-for="task in missionStore.mission.tasks"
|
||||
:key="task.id"
|
||||
@ -197,6 +197,14 @@
|
||||
<n-button block @click="showTypeModal = false">取消</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<!-- Planner modal overlay -->
|
||||
<div v-if="showPlanner" style="position:fixed;inset:0;z-index:1000;background:#fff;overflow:hidden;">
|
||||
<PathPlannerView
|
||||
:task-id="plannerTaskId"
|
||||
:sub-task-id="plannerSubTaskId"
|
||||
@close="showPlanner = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -217,6 +225,7 @@ import type { SubTask } from '../types/task';
|
||||
import type { PathLineRecord } from '../types/path-line';
|
||||
import type { DataTableColumn } from 'naive-ui';
|
||||
import HelpIcon from '../components/common/HelpIcon.vue';
|
||||
import PathPlannerView from '../views/PathPlannerView.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const message = useMessage();
|
||||
@ -224,6 +233,16 @@ const dialog = useDialog();
|
||||
const missionStore = useMissionStore();
|
||||
const validationStore = useValidationStore();
|
||||
|
||||
const expandedTask = computed({
|
||||
get: () => missionStore.expandedTaskId,
|
||||
set: (v) => (missionStore.expandedTaskId = v),
|
||||
});
|
||||
|
||||
const showPlanner = ref(false);
|
||||
const plannerTaskId = ref<number | null>(null);
|
||||
const plannerSubTaskId = ref<string | null>(null);
|
||||
|
||||
// function onExpandedChange(val: string | null) {
|
||||
const showTypeModal = ref(false);
|
||||
const typeModalTaskId = ref<number | null>(null);
|
||||
|
||||
@ -364,9 +383,10 @@ async function browsePathLine(sub: SubTask) {
|
||||
}
|
||||
|
||||
function goToPlanner(taskId: number, sub: SubTask) {
|
||||
router.push({ path: '/planner', query: { taskId: String(taskId), subTaskId: sub.id } });
|
||||
plannerTaskId.value = taskId;
|
||||
plannerSubTaskId.value = sub.id;
|
||||
showPlanner.value = true;
|
||||
}
|
||||
|
||||
async function openPathFile(path: string) {
|
||||
try {
|
||||
const file = await invoke('load_path_line', { path });
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="path-planner">
|
||||
<n-space style="padding: 8px" align="center">
|
||||
<n-button size="small" @click="goBack">
|
||||
<n-button size="small" @click="$emit('close')">
|
||||
<template #icon><n-icon><ArrowBackOutline /></n-icon></template>
|
||||
返回
|
||||
</n-button>
|
||||
<n-button size="small" :type="drawingMode && currentShape === 'Rect' ? 'warning' : 'default'" @click="startDraw('Rect')">
|
||||
<template #icon><n-icon><SquareOutline /></n-icon></template>
|
||||
画框
|
||||
矩形
|
||||
</n-button>
|
||||
<n-button size="small" :type="drawingMode && currentShape === 'Circle' ? 'warning' : 'default'" @click="startDraw('Circle')">
|
||||
<template #icon><n-icon><EllipseOutline /></n-icon></template>
|
||||
@ -15,7 +15,11 @@
|
||||
</n-button>
|
||||
<n-button size="small" @click="clearRects" :disabled="regions.length === 0">
|
||||
<template #icon><n-icon><TrashOutline /></n-icon></template>
|
||||
清除
|
||||
清空
|
||||
</n-button>
|
||||
<n-button size="small" @click="deleteSelected" :disabled="selectedIndex < 0">
|
||||
<template #icon><n-icon><CloseOutline /></n-icon></template>
|
||||
删除
|
||||
</n-button>
|
||||
<n-button size="small" type="primary" @click="generatePath" :disabled="regions.length === 0">
|
||||
<template #icon><n-icon><MapOutline /></n-icon></template>
|
||||
@ -30,12 +34,15 @@
|
||||
<n-split direction="horizontal" :default-size="0.6">
|
||||
<template #1>
|
||||
<ScanCanvas
|
||||
:regions="regions"
|
||||
v-model:regions="regions"
|
||||
:records="previewRecords"
|
||||
:mode="scanMode"
|
||||
:background-image="missionStore.mission.backgroundImage || undefined"
|
||||
:drawing-mode="drawingMode"
|
||||
:drawing-shape="currentShape"
|
||||
:selected-index="selectedIndex"
|
||||
@select-region="selectedIndex = $event"
|
||||
@deselect="selectedIndex = -1"
|
||||
@add-region="onAddRegion"
|
||||
/>
|
||||
</template>
|
||||
@ -58,9 +65,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { ArrowBackOutline, SquareOutline, EllipseOutline, TrashOutline, MapOutline, SaveOutline } from '@vicons/ionicons5';
|
||||
import { ArrowBackOutline, SquareOutline, EllipseOutline, TrashOutline, CloseOutline, MapOutline, SaveOutline } from '@vicons/ionicons5';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import ScanCanvas from '../components/planner/ScanCanvas.vue';
|
||||
import ScanParamPanel from '../components/planner/ScanParamPanel.vue';
|
||||
@ -70,21 +76,23 @@ import type { ScanRegion, CameraParams, ScanMode, ScanShape } from '../types/pat
|
||||
import type { PlannerDefaults } from '../types/mission';
|
||||
import { DEVICE_FOV } from '../utils/constants';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const props = defineProps<{
|
||||
taskId: number | null;
|
||||
subTaskId: string | null;
|
||||
}>();
|
||||
|
||||
defineEmits<{ close: [] }>();
|
||||
|
||||
const message = useMessage();
|
||||
const missionStore = useMissionStore();
|
||||
|
||||
const taskId = route.query.taskId ? Number(route.query.taskId) : null;
|
||||
const subTaskId = route.query.subTaskId ? String(route.query.subTaskId) : null;
|
||||
|
||||
const drawingMode = ref(false);
|
||||
const currentShape = ref<ScanShape>('Rect');
|
||||
|
||||
function getDeviceFov(): number {
|
||||
if (taskId === null || subTaskId === null) return 30;
|
||||
const task = missionStore.mission.tasks.find(t => t.id === taskId);
|
||||
const sub = task?.subTasks.find(s => s.id === subTaskId);
|
||||
if (props.taskId === null || props.subTaskId === null) return 30;
|
||||
const task = missionStore.mission.tasks.find(t => t.id === props.taskId);
|
||||
const sub = task?.subTasks.find(s => s.id === props.subTaskId);
|
||||
return (sub && DEVICE_FOV[sub.type]) || 30;
|
||||
}
|
||||
|
||||
@ -94,13 +102,12 @@ const coverageRate = ref(30);
|
||||
const speedY = ref(5);
|
||||
const speedXScan = ref(10);
|
||||
const speedXStart = ref(20);
|
||||
const scanMode = ref<ScanMode>('Zigzag');
|
||||
const scanMode = ref<ScanMode>('OneWay');
|
||||
|
||||
const generatedRecords = ref<PathLineRecord[]>([]);
|
||||
const previewRecords = computed(() => generatedRecords.value);
|
||||
const estimatedTime = ref(0);
|
||||
|
||||
// Load saved planner defaults
|
||||
invoke<PlannerDefaults>('load_planner_defaults').then(p => {
|
||||
coverageRate.value = p.coverageRate;
|
||||
speedY.value = p.speedYCmS;
|
||||
@ -111,38 +118,32 @@ invoke<PlannerDefaults>('load_planner_defaults').then(p => {
|
||||
}).catch(() => {});
|
||||
|
||||
function startDraw(shape: ScanShape) {
|
||||
if (drawingMode.value && currentShape.value === shape) {
|
||||
drawingMode.value = false;
|
||||
} else {
|
||||
drawingMode.value = true;
|
||||
currentShape.value = shape;
|
||||
}
|
||||
drawingMode.value = true;
|
||||
currentShape.value = shape;
|
||||
}
|
||||
|
||||
function onAddRegion(region: ScanRegion) {
|
||||
region.shape = currentShape.value;
|
||||
regions.value.push(region);
|
||||
drawingMode.value = false;
|
||||
message.success(`已添加框 #${regions.value.length}`);
|
||||
}
|
||||
|
||||
function clearRects() {
|
||||
regions.value = [];
|
||||
generatedRecords.value = [];
|
||||
estimatedTime.value = 0;
|
||||
message.info('已清除所有框');
|
||||
}
|
||||
const selectedIndex = ref(-1);
|
||||
function clearRects() { regions.value = []; generatedRecords.value = []; estimatedTime.value = 0; message.info('已清空所有框'); }
|
||||
|
||||
function savePlannerDefaults() {
|
||||
const p: PlannerDefaults = {
|
||||
fovDegrees: camera.value.fovDegrees,
|
||||
heightCm: camera.value.heightCm,
|
||||
coverageRate: coverageRate.value,
|
||||
speedYCmS: speedY.value,
|
||||
speedXStartCmS: speedXStart.value,
|
||||
speedXScanCmS: speedXScan.value,
|
||||
mode: scanMode.value,
|
||||
};
|
||||
invoke('save_planner_defaults', { config: p }).catch(() => {});
|
||||
invoke('save_planner_defaults', {
|
||||
config: { fovDegrees: camera.value.fovDegrees, heightCm: camera.value.heightCm, coverageRate: coverageRate.value, speedYCmS: speedY.value, speedXStartCmS: speedXStart.value, speedXScanCmS: speedXScan.value, mode: scanMode.value },
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function deleteSelected() {
|
||||
if (selectedIndex.value < 0) return;
|
||||
regions.value.splice(selectedIndex.value, 1);
|
||||
selectedIndex.value = -1;
|
||||
generatedRecords.value = [];
|
||||
estimatedTime.value = 0;
|
||||
}
|
||||
|
||||
function onParamChange(params: any) {
|
||||
@ -159,68 +160,37 @@ async function generatePath() {
|
||||
if (!regions.value.length) return;
|
||||
try {
|
||||
const file = await invoke<PathLineFile>('generate_scan_paths_multi', {
|
||||
regions: regions.value,
|
||||
shapes: regions.value.map(r => r.shape),
|
||||
camera: camera.value,
|
||||
coverageRate: coverageRate.value,
|
||||
speedYCmS: speedY.value,
|
||||
speedXStartCmS: speedXStart.value,
|
||||
speedXScanCmS: speedXScan.value,
|
||||
mode: scanMode.value,
|
||||
regions: regions.value, shapes: regions.value.map(r => r.shape),
|
||||
camera: camera.value, coverageRate: coverageRate.value,
|
||||
speedYCmS: speedY.value, speedXStartCmS: speedXStart.value, speedXScanCmS: speedXScan.value, mode: scanMode.value,
|
||||
});
|
||||
generatedRecords.value = file.records;
|
||||
const estTime = await invoke<number>('estimate_scan_time_multi_minutes', {
|
||||
regions: regions.value,
|
||||
shapes: regions.value.map(r => r.shape),
|
||||
camera: camera.value,
|
||||
coverageRate: coverageRate.value,
|
||||
speedYCmS: speedY.value,
|
||||
speedXStartCmS: speedXStart.value,
|
||||
speedXScanCmS: speedXScan.value,
|
||||
mode: scanMode.value,
|
||||
regions: regions.value, shapes: regions.value.map(r => r.shape),
|
||||
camera: camera.value, coverageRate: coverageRate.value,
|
||||
speedYCmS: speedY.value, speedXStartCmS: speedXStart.value, speedXScanCmS: speedXScan.value, mode: scanMode.value,
|
||||
});
|
||||
estimatedTime.value = estTime;
|
||||
message.success(`航线生成完成: ${file.count} 条记录`);
|
||||
} catch (e) {
|
||||
message.error('航线生成失败: ' + e);
|
||||
}
|
||||
} catch (e) { message.error('航线生成失败: ' + e); }
|
||||
}
|
||||
|
||||
async function savePath() {
|
||||
if (!generatedRecords.value.length) return;
|
||||
try {
|
||||
const { save } = await import('@tauri-apps/plugin-dialog');
|
||||
const path = await save({
|
||||
filters: [{ name: 'RecordLine3', extensions: ['RecordLine3'] }],
|
||||
});
|
||||
const path = await save({ filters: [{ name: 'RecordLine3', extensions: ['RecordLine3'] }] });
|
||||
if (path) {
|
||||
const finalPath = path.endsWith('.RecordLine3') ? path : path + '.RecordLine3';
|
||||
const file: PathLineFile = {
|
||||
count: generatedRecords.value.length,
|
||||
records: generatedRecords.value,
|
||||
};
|
||||
await invoke('save_path_line', { path: finalPath, file });
|
||||
await invoke('save_path_line', { path: finalPath, file: { count: generatedRecords.value.length, records: generatedRecords.value } });
|
||||
message.success('航线保存成功');
|
||||
|
||||
if (taskId !== null && subTaskId !== null) {
|
||||
const task = missionStore.mission.tasks.find(t => t.id === taskId);
|
||||
if (task) {
|
||||
const sub = task.subTasks.find(s => s.id === subTaskId);
|
||||
if (sub) {
|
||||
sub.pathLineFilePath = finalPath;
|
||||
await missionStore.updateSubTask(taskId, { ...sub });
|
||||
message.success('已自动填入子任务航线路径');
|
||||
}
|
||||
}
|
||||
if (props.taskId !== null && props.subTaskId !== null) {
|
||||
const task = missionStore.mission.tasks.find(t => t.id === props.taskId);
|
||||
const sub = task?.subTasks.find(s => s.id === props.subTaskId);
|
||||
if (sub) { sub.pathLineFilePath = finalPath; await missionStore.updateSubTask(props.taskId, { ...sub }); message.success('已自动填入子任务航线路径'); }
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
message.error('保存失败: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/editor');
|
||||
} catch (e) { message.error('保存失败: ' + e); }
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user