From bdb27be4de2aba39462a3e741cbd31ec025fd9e5 Mon Sep 17 00:00:00 2001
From: xiaosi <2652281683@qq.com>
Date: Tue, 1 Sep 2026 14:32:50 +0800
Subject: [PATCH] =?UTF-8?q?docs:=20=E8=88=AA=E7=BA=BF=E5=BA=93=E7=BC=96?=
=?UTF-8?q?=E8=BE=91=E5=88=A0=E9=99=A4=E5=AE=9E=E7=8E=B0=E8=AE=A1=E5=88=92?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
按已批准 spec 拆分 URL、RouteCard、删除与编辑回填保存任务。
---
.../2026-09-01-route-library-edit-delete.md | 401 ++++++++++++++++++
1 file changed, 401 insertions(+)
create mode 100644 docs/superpowers/plans/2026-09-01-route-library-edit-delete.md
diff --git a/docs/superpowers/plans/2026-09-01-route-library-edit-delete.md b/docs/superpowers/plans/2026-09-01-route-library-edit-delete.md
new file mode 100644
index 0000000..dd9254d
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-01-route-library-edit-delete.md
@@ -0,0 +1,401 @@
+# 航线库编辑 / 删除 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;`TasksView` 用 `editingRouteId` 区分新建/编辑,编辑时拉详情回填后 `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 段改为:
+
+```js
+// Routes
+export const ROUTES = '/v1/routes'
+export const ROUTE = (id) => `/v1/routes/${id}`
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+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`**
+
+完整替换为:
+
+```vue
+
+
+
+
+ {{ distance }}
+
+
+
{{ name }}
+
{{ waypoints }} 个航点 · 采集于 {{ collectedAt }}
+
+ 编辑
+ 删除
+
+
+
+
+
+
+```
+
+- [ ] **Step 2: 样式**
+
+在 `prototype.css` 的 `.route-grid small{...}` 之后插入(并修正 body 选择器,避免 actions 被当成 last-child 旧规则误伤):
+
+把:
+
+```css
+.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; }
+```
+
+替换为:
+
+```css
+.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**
+
+```bash
+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` 用法改为:
+
+```vue
+
+```
+
+(`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` 附近,模式对齐:
+
+```js
+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**
+
+```bash
+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` 旁增加:
+
+```js
+const editingRouteId = ref(null)
+```
+
+- [ ] **Step 2: 航点映射 helper**
+
+在 route editor 函数区增加:
+
+```js
+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 新建**
+
+```js
+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`**
+
+```js
+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()` 一次。若 `attachRouteMap` 的 `load`/`style.load` 回调末尾没有 sync,在该回调末尾对 `routePoints` 调用 `syncRouteLayer()`,或 `openRouteEditorForEdit` 在 `routeMapReady` 为 true 后调用。实现时读现有 `attachRouteMap` 并接到已有 ready 路径,避免双绑 click。
+
+- [ ] **Step 5: 模板 header 文案**
+
+```vue
+
+ {{ editingRouteId ? '编辑航线' : '新建航线' }}
+ {{ routeSummary }}
+
+...
+
+ {{ routeSaving ? '保存中…' : (editingRouteId ? '保存修改' : '保存航线') }}
+
+```
+
+- [ ] **Step 6: `submitRoute` 分支**
+
+替换保存核心为:
+
+```js
+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: 构建验收**
+
+```bash
+npm run build
+```
+
+Expected: exit 0。
+
+本地 `npm run dev` 或 preview:
+
+1. 新建仍可用
+2. 编辑回填名称/航点,地图有线
+3. 保存修改后列表更新
+4. 删除确认后列表减少
+
+- [ ] **Step 8: Commit**
+
+```bash
+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: 若有未提交改动则提交**
+
+```bash
+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 + 手工代替单测。