1 changed files with 401 additions and 0 deletions
@ -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 |
|||
<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 旧规则误伤): |
|||
|
|||
把: |
|||
|
|||
```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 |
|||
<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` 附近,模式对齐: |
|||
|
|||
```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 |
|||
<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` 分支** |
|||
|
|||
替换保存核心为: |
|||
|
|||
```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 + 手工代替单测。 |
|||
Loading…
Reference in new issue