You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

11 KiB

航线库编辑 / 删除 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 在任务管理 → 航线库为每条航线补齐卡片「编辑 / 删除」,编辑复用现有地图航线编辑器,分别对接 GET/PUT/DELETE /v1/routes/:id

Architecture: 前端 only。urls.ROUTE(id) 提供详情/更新/删除路径;RouteCard 增加操作条并 emit;TasksVieweditingRouteId 区分新建/编辑,编辑时拉详情回填后 PUT,删除走 ui.confirm + DELETE。不改后端、不新增测试框架(仓库无 vitest/jest)。

Tech Stack: Vue 3、TDesign Vue Next、Pinia ui.confirm、axios request、现有 Mapbox 航线编辑器。

Spec: docs/superpowers/specs/2026-09-01-route-library-edit-delete-design.md


File map

文件 职责
src/config/urls.js 新增 ROUTE(id)
src/components/RouteCard.vue 卡片展示 + 编辑/删除按钮
src/views/TasksView/TasksView.vue 列表事件、编辑打开/保存分支、删除
src/styles/prototype.css 卡片操作条样式

Task 1: URL helper

Files:

  • Modify: src/config/urls.js

  • Step 1: 增加 ROUTE

在 Routes 段改为:

// Routes
export const ROUTES = '/v1/routes'
export const ROUTE = (id) => `/v1/routes/${id}`
  • Step 2: Commit
git add src/config/urls.js
git commit -m "feat(routes): add ROUTE(id) url helper"

Task 2: RouteCard 操作条

Files:

  • Modify: src/components/RouteCard.vue

  • Modify: src/styles/prototype.css(route-grid 段附近)

  • Step 1: 重写 RouteCard.vue

完整替换为:

<template>
  <article class="route-card">
    <div class="route-map" :class="mapClass">
      <svg><use href="#i-route" /></svg>
      <span>{{ distance }}</span>
    </div>
    <div class="route-card-body">
      <strong>{{ name }}</strong>
      <small>{{ waypoints }} 个航点 · 采集于 {{ collectedAt }}</small>
      <div class="route-card-actions" @click.stop>
        <t-button variant="text" theme="primary" type="button" @click="emit('edit')">编辑</t-button>
        <t-button variant="text" theme="danger" type="button" @click="emit('delete')">删除</t-button>
      </div>
    </div>
  </article>
</template>

<script setup>
defineProps({
  name: { type: String, required: true },
  waypoints: { type: Number, required: true },
  distance: { type: String, required: true },
  collectedAt: { type: String, required: true },
  mapClass: { type: String, required: true }
})

const emit = defineEmits(['edit', 'delete'])
</script>
  • Step 2: 样式

prototype.css.route-grid small{...} 之后插入(并修正 body 选择器,避免 actions 被当成 last-child 旧规则误伤):

把:

.route-grid article > div:last-child{ padding: 11px 12px; display: flex; flex-direction: column; gap: 4px; }
.route-grid strong{ font-size: 12px; }
.route-grid small{ color: var(--muted); font-size: 10px; }

替换为:

.route-grid article > .route-card-body,
.route-grid article > div:last-child{ padding: 11px 12px; display: flex; flex-direction: column; gap: 4px; }
.route-grid strong{ font-size: 12px; }
.route-grid small{ color: var(--muted); font-size: 10px; }
.route-card-actions{ margin-top: 8px; display: flex; align-items: center; gap: 2px; }
.route-card-actions .t-button{ height: 28px; }
  • Step 3: Commit
git add src/components/RouteCard.vue src/styles/prototype.css
git commit -m "feat(routes): add edit/delete actions on RouteCard"

Task 3: TasksView 删除航线

Files:

  • Modify: src/views/TasksView/TasksView.vue

  • Step 1: 模板绑定删除

将航线库 RouteCard 用法改为:

<RouteCard
  v-for="r in filteredRoutes"
  :key="r.id"
  :name="r.name"
  :waypoints="r.waypoints"
  :distance="r.distance"
  :collected-at="r.collectedAt"
  :map-class="r.mapClass"
  @edit="openRouteEditorForEdit(r)"
  @delete="deleteRoute(r)"
/>

openRouteEditorForEdit 在 Task 4 实现;本 task 可先写函数 stub 或与 Task 4 同一提交。推荐本 task 只接 @delete@edit 留 Task 4——若一次改模板,两个 handler 都要存在。)

推荐一次改模板,Task 3+4 逻辑可同一 commit 或分 commit;下面按可独立验证的顺序写。

本 step 模板同时接两个事件;Task 3 实现 deleteRoute,Task 4 实现 edit。

  • Step 2: 实现 deleteRoute

放在 deleteTask 附近,模式对齐:

async function deleteRoute(route) {
  const ok = await ui.confirm(
    `确认删除航线「${route.name}」?`,
    '删除后不可恢复。若已有任务绑定该航线,请自行调整任务配置。',
    ['移除航线记录', '同步删除全部航点', '不可恢复']
  )
  if (!ok) return
  try {
    await request.delete(urls.ROUTE(route.id))
    ui.toast('航线已删除')
    await load()
  } catch (e) {
    ui.toast(e.message || '删除航线失败')
  }
}

确认 import * as urls / urls 对象已包含将添加的 ROUTE(现有文件已 import * as urls from '@/config/urls' 或等价——以文件实际 import 为准,使用 urls.ROUTE)。

  • Step 3: 冒烟

  • 有 token 时打开航线库,卡片出现删除

  • 取消确认 → 列表不变

  • (可选)真删一条测试数据

  • Step 4: Commit

git add src/views/TasksView/TasksView.vue
git commit -m "feat(routes): delete route from library card"

Task 4: TasksView 编辑航线(打开 + 保存)

Files:

  • Modify: src/views/TasksView/TasksView.vue

  • Step 1: 状态

routeEditorOpen 旁增加:

const editingRouteId = ref(null)
  • Step 2: 航点映射 helper

在 route editor 函数区增加:

function mapApiWaypointToPoint(wp) {
  return {
    longitude: Number(wp.longitude),
    latitude: Number(wp.latitude),
    altitude: Number(wp.altitude),
    speed: Number(wp.speed),
    turnMode: Number(wp.holdSec) > 0 ? 'stop' : 'auto',
  }
}

function mapPointToApiWaypoint(point) {
  return {
    longitude: point.longitude,
    latitude: point.latitude,
    altitude: point.altitude,
    speed: point.speed,
    yaw: 0,
    holdSec: point.turnMode === 'stop' ? 1 : 0,
  }
}
  • Step 3: 调整 reset / close / open 新建
function resetRouteEditor() {
  Object.assign(routeForm, { name: '', altitude: 50, speed: 10, turnMode: 'auto' })
  routePoints.value = []
  routeFormError.value = ''
  routeCursor.value = '--'
  editingRouteId.value = null
}

function openRouteEditor() {
  resetRouteEditor()
  routeEditorOpen.value = true
  attachRouteMap()
}

function closeRouteEditor() {
  teardownRouteMap()
  routeEditorOpen.value = false
  waypointEditorOpen.value = false
  editingRouteId.value = null
}

(若 closeRouteEditor 已有其它逻辑,保留并只补 editingRouteId 清理;resetRouteEditor 已在 open 时调用则可只在 reset/close 一处清空 id。)

  • Step 4: openRouteEditorForEdit
async function openRouteEditorForEdit(route) {
  routeFormError.value = ''
  try {
    const detail = await request.get(urls.ROUTE(route.id))
    resetRouteEditor()
    editingRouteId.value = String(detail?.id ?? route.id)
    const list = Array.isArray(detail?.waypoints) ? detail.waypoints : []
    routePoints.value = list.map(mapApiWaypointToPoint)
    const first = routePoints.value[0]
    Object.assign(routeForm, {
      name: detail?.name || route.name || '',
      altitude: first ? first.altitude : 50,
      speed: first ? first.speed : 10,
      turnMode: first ? first.turnMode : 'auto',
    })
    routeEditorOpen.value = true
    attachRouteMap()
    // attach 后需把点画上:现有 syncRouteLayer 在 map ready 后应被调用
    // 若 attachRouteMap 末尾不自动 sync,则在 nextTick + map ready 回调里 syncRouteLayer()
    await Promise.resolve()
    syncRouteLayer()
  } catch (e) {
    ui.toast(e.message || '加载航线详情失败')
  }
}

注意: 检查 attachRouteMap / syncRouteLayer 时序。若 map 异步 ready,现有新建是点选时才 sync;编辑打开后必须在 map ready 后 syncRouteLayer() 一次。若 attachRouteMapload/style.load 回调末尾没有 sync,在该回调末尾对 routePoints 调用 syncRouteLayer(),或 openRouteEditorForEditrouteMapReady 为 true 后调用。实现时读现有 attachRouteMap 并接到已有 ready 路径,避免双绑 click。

  • Step 5: 模板 header 文案
<div>
  <strong>{{ editingRouteId ? '编辑航线' : '新建航线' }}</strong>
  <span>{{ routeSummary }}</span>
</div>
...
<t-button theme="primary" type="button" :disabled="routeSaving" @click="submitRoute">
  {{ routeSaving ? '保存中…' : (editingRouteId ? '保存修改' : '保存航线') }}
</t-button>
  • Step 6: submitRoute 分支

替换保存核心为:

async function submitRoute() {
  const name = routeForm.name.trim()
  if (!name) {
    routeFormError.value = '请输入航线名称。'
    return
  }
  if (routePoints.value.length < 2) {
    routeFormError.value = '至少需要设置 2 个航点。'
    return
  }
  const payload = {
    name,
    waypoints: routePoints.value.map(mapPointToApiWaypoint),
  }
  routeSaving.value = true
  try {
    if (editingRouteId.value) {
      await request.put(urls.ROUTE(editingRouteId.value), payload)
      ui.toast('航线已更新')
    } else {
      await request.post(urls.ROUTES, payload)
      ui.toast('航线已保存')
    }
    closeRouteEditor()
    await load()
  } catch (e) {
    routeFormError.value = e.message || (editingRouteId.value ? '更新航线失败' : '保存航线失败')
  } finally {
    routeSaving.value = false
  }
}
  • Step 7: 构建验收
npm run build

Expected: exit 0。

本地 npm run dev 或 preview:

  1. 新建仍可用
  2. 编辑回填名称/航点,地图有线
  3. 保存修改后列表更新
  4. 删除确认后列表减少
  • Step 8: Commit
git add src/views/TasksView/TasksView.vue
git commit -m "feat(routes): edit existing route in library editor"

Task 5: 回归与收尾

  • Step 1: 快速回归清单

  • 航线库搜索仍过滤名称

  • 新建任务下拉仍能选航线(routeOptions

  • 编辑器返回航线库 hidden 切换正常

  • 航点单点编辑弹窗仍可用

  • Step 2: 若有未提交改动则提交

git status
  • Step 3: 实现完成后 使用 finishing-a-development-branch(或等价)整理分支 / 是否部署,由用户决定是否发远端。

Spec coverage check

Spec 项 Task
ROUTE(id) T1
卡片编辑/删除 UI T2
删除 confirm + DELETE T3
GET 详情回填 + PUT T4
新建不变 T4 open/submit 分支
标题/按钮文案 T4
航点 holdSec↔turnMode T4 helpers
样式 T2
错误处理 T3/T4
非目标未做 全任务 YAGNI

Placeholder scan

无 TBD/TODO 步骤;无测试框架故用 build + 手工代替单测。