Переглянути джерело

chore: mark all plan steps complete

xuyunhui 1 тиждень тому
батько
коміт
c1fe4c02a5

+ 705 - 0
docs/superpowers/plans/2026-06-24-weapp-fixes-and-schedule-creation.md

@@ -0,0 +1,705 @@
+# 清道夫 App 5 项修复与排班新增实施计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
+
+**Goal:** 修复 tab 页 TopBar 返回按钮、任务页胶囊遮挡、tabBar 高亮同步、退出登录卡顿问题,并实现排班页新增排班功能。
+
+**Architecture:** 前端基于 uni-app Vue 3 + TypeScript + Pinia,通过修改通用组件与页面生命周期解决问题;后端 Spring Boot 补全 `/vehicle/simple-list` 下拉接口;排班新增采用弹窗表单,严格映射后端 `DispatchSchedule` 字段。
+
+**Tech Stack:** uni-app Vue 3, TypeScript, Tailwind, Pinia, Spring Boot, MyBatis-Plus
+
+## Global Constraints
+
+- 所有 API 以后端接口为准;缺失则后端补全。
+- 排班类型映射:月计划 ↔ `month_plan`,应急 ↔ `emergency`。
+- 保持 `npm run type-check` 与 `npm run build:mp-weixin` 通过。
+- 每个任务完成后提交一次 git commit。
+
+---
+
+## Task 1: 后端补全 `/vehicle/simple-list` 接口
+
+**Files:**
+- Modify: `D:/work/绿水青山/java/aquagreen-server/aquagreen-business/src/main/java/com/aquagreen/business/service/VehicleService.java`
+- Modify: `D:/work/绿水青山/java/aquagreen-server/aquagreen-business/src/main/java/com/aquagreen/business/controller/VehicleController.java`
+
+**Interfaces:**
+- Produces: `GET /vehicle/simple-list` → `R<List<Map<String, Object>>>` with `{ value: vehicleId, label: plateNo }`
+
+- [x] **Step 1.1: 在 VehicleService 添加 simpleList 方法**
+
+在 `VehicleService.java` 中 `assignToTask` 方法之后新增:
+
+```java
+public List<Map<String, Object>> simpleList() {
+    List<BizVehicle> list = list(
+        new LambdaQueryWrapper<BizVehicle>()
+            .orderByAsc(BizVehicle::getPlateNo));
+    List<Map<String, Object>> result = new ArrayList<>();
+    for (BizVehicle vehicle : list) {
+        Map<String, Object> item = new HashMap<>();
+        item.put("value", vehicle.getVehicleId());
+        item.put("label", vehicle.getPlateNo());
+        result.add(item);
+    }
+    return result;
+}
+```
+
+并在文件顶部添加 `import java.util.ArrayList;` 和 `import java.util.HashMap;`(如不存在)。
+
+- [x] **Step 1.2: 在 VehicleController 添加 /vehicle/simple-list 端点**
+
+在 `VehicleController.java` 中 `/vehicle/{vehicleId}` 之前新增:
+
+```java
+@GetMapping("/simple-list")
+@Operation(summary = "车辆下拉列表", description = "返回车辆ID与车牌号,用于下拉选择")
+public R<List<Map<String, Object>>> simpleList() {
+    return R.ok(vehicleService.simpleList());
+}
+```
+
+并在文件顶部添加 `import java.util.List;`、`import java.util.Map;`、`import java.util.HashMap;`、`import java.util.ArrayList;`(如不存在)。
+
+- [x] **Step 1.3: 后端编译验证**
+
+Run: `cd D:/work/绿水青山/java/aquagreen-server && mvn compile -pl aquagreen-business -am`
+Expected: BUILD SUCCESS
+
+- [x] **Step 1.4: Commit**
+
+```bash
+git add aquagreen-business/src/main/java/com/aquagreen/business/service/VehicleService.java aquagreen-business/src/main/java/com/aquagreen/business/controller/VehicleController.java
+git commit -m "feat(vehicle): add /vehicle/simple-list endpoint"
+```
+
+---
+
+## Task 2: 修复 TopBar 默认返回按钮与 tab 页面返回按钮
+
+**Files:**
+- Modify: `D:/work/绿水青山/清道夫App/src/components/common/TopBar.vue`
+- Modify: `D:/work/绿水青山/清道夫App/src/components/dispatch/ScheduleView.vue`(可选,确认 showBack 传递)
+
+**Interfaces:**
+- Consumes: `TopBar.showBack` prop(Boolean,默认 false)
+- Produces: tab 页面不再显示返回按钮
+
+- [x] **Step 2.1: 修改 TopBar 默认值**
+
+在 `TopBar.vue` 中,将 `showBack` 默认值从 `true` 改为 `false`:
+
+```typescript
+const props = withDefaults(
+  defineProps<{
+    title: string
+    showBack?: boolean
+  }>(),
+  {
+    showBack: false
+  }
+)
+```
+
+- [x] **Step 2.2: 全局搜索确认非 tab 页面已显式开启返回(如需要)**
+
+Run: `grep -rn "<TopBar" src/`
+Expected: 非 tab 页面(如任务详情、发布页)若依赖返回,应已显式传入 `:show-back="true"`;本次仅修复 tab 页面默认值问题,不扩大范围。
+
+- [x] **Step 2.3: Commit**
+
+```bash
+git add src/components/common/TopBar.vue
+git commit -m "fix(topbar): default showBack to false for tab pages"
+```
+
+---
+
+## Task 3: 修复任务页发布/新增按钮被胶囊遮挡
+
+**Files:**
+- Modify: `D:/work/绿水青山/清道夫App/src/components/task/TaskListView.vue`
+
+**Interfaces:**
+- Consumes: `TopBar` right slot 已包裹 `CapsuleSafeArea`
+- Produces: 按钮完整可见且可点击
+
+当前 `TaskListView.vue` 中按钮已放入 `TopBar` right slot,但胶囊仍可能遮挡。原因:`button` 元素本身有 `bg-primary` 等样式,`CapsuleSafeArea` 仅给外层容器加了 `margin-left`;若按钮宽度或右侧 padding 导致溢出,需要调整。
+
+- [x] **Step 3.1: 为 right slot 按钮添加更明显的内边距控制**
+
+将 `TaskListView.vue` 中第 3-14 行替换为:
+
+```vue
+<TopBar :title="pageTitle">
+  <template #right>
+    <view class="flex items-center pr-2">
+      <button
+        v-if="config.publishPath"
+        class="px-3 py-1.5 bg-primary text-white rounded-lg text-xs flex items-center whitespace-nowrap"
+        @click="goToPublishTask"
+      >
+        <text class="uni-icons uniui-plusempty mr-1"></text>
+        {{ publishText }}
+      </button>
+    </view>
+  </template>
+</TopBar>
+```
+
+- [x] **Step 3.2: 验证构建无 TS/模板错误**
+
+Run: `npm run type-check`
+Expected: 无错误
+
+- [x] **Step 3.3: Commit**
+
+```bash
+git add src/components/task/TaskListView.vue
+git commit -m "fix(task): ensure publish button stays clear of WeChat capsule"
+```
+
+---
+
+## Task 4: 修复自定义 tabBar 高亮同步
+
+**Files:**
+- Modify: `D:/work/绿水青山/清道夫App/src/pages/home/home.vue`
+- Modify: `D:/work/绿水青山/清道夫App/src/pages/task/task.vue`
+- Modify: `D:/work/绿水青山/清道夫App/src/pages/schedule/schedule.vue`
+- Modify: `D:/work/绿水青山/清道夫App/src/pages/my/my.vue`
+- Modify: `D:/work/绿水青山/清道夫App/src/custom-tab-bar/index.js`
+
+**Interfaces:**
+- Consumes: `getTabBar()` API from custom tabBar component
+- Produces: 切换 tab 后高亮即时同步
+
+- [x] **Step 4.1: 在 custom-tab-bar/index.js 中暴露 setSelected 方法**
+
+在 `custom-tab-bar/index.js` 的 `methods` 对象中新增:
+
+```javascript
+setSelected(index) {
+  this.setData({ selected: index })
+}
+```
+
+- [x] **Step 4.2: 在 tab 页面 onShow 中同步高亮**
+
+在每个 tab 页面的 `onShow` 中添加(需从 `@dcloudio/uni-app` 导入 `onShow`):
+
+**src/pages/home/home.vue**(索引 0):
+
+```typescript
+import { onShow } from '@dcloudio/uni-app'
+
+onShow(() => {
+  const tabBar = getTabBar ? getTabBar() : null
+  tabBar?.setSelected?.(0)
+})
+```
+
+**src/pages/task/task.vue**(索引 1,commonTabs)或 2(dispatchTabs):
+
+```typescript
+onShow(() => {
+  taskStore.fetchTasks()
+  const tabBar = getTabBar ? getTabBar() : null
+  const role = authStore.userRole
+  const index = role === 'dispatch' ? 2 : 1
+  tabBar?.setSelected?.(index)
+})
+```
+
+**src/pages/schedule/schedule.vue**(索引 1,仅 dispatch):
+
+```vue
+<script setup lang="ts">
+import { onShow } from '@dcloudio/uni-app'
+import ScheduleView from '@/components/dispatch/ScheduleView.vue'
+
+onShow(() => {
+  const tabBar = getTabBar ? getTabBar() : null
+  tabBar?.setSelected?.(1)
+})
+</script>
+```
+
+**src/pages/my/my.vue**(commonTabs 索引 2,dispatchTabs 索引 3):
+
+```vue
+<script setup lang="ts">
+import { onShow } from '@dcloudio/uni-app'
+import { useAuthStore } from '@/stores/auth'
+import ProfileView from '@/components/my/ProfileView.vue'
+
+const authStore = useAuthStore()
+
+onShow(() => {
+  const tabBar = getTabBar ? getTabBar() : null
+  const role = authStore.userRole
+  const index = role === 'dispatch' ? 3 : 2
+  tabBar?.setSelected?.(index)
+})
+</script>
+```
+
+- [x] **Step 4.3: 验证 type-check**
+
+Run: `npm run type-check`
+Expected: 无错误(`getTabBar` 为 uni-app 全局 API,TypeScript 可能需要 `declare const getTabBar: any` 或忽略)
+
+- [x] **Step 4.4: Commit**
+
+```bash
+git add src/pages/home/home.vue src/pages/task/task.vue src/pages/schedule/schedule.vue src/pages/my/my.vue src/custom-tab-bar/index.js
+git commit -m "fix(tabbar): sync highlight on tab page onShow"
+```
+
+---
+
+## Task 5: 修复退出登录卡顿
+
+**Files:**
+- Modify: `D:/work/绿水青山/清道夫App/src/components/my/ProfileView.vue`
+
+**Interfaces:**
+- Consumes: `uni.reLaunch`, `authStore.doLogout`
+- Produces: 登出动画流畅
+
+- [x] **Step 5.1: 调整 handleLogout 顺序**
+
+将 `ProfileView.vue` 中 `handleLogout` 函数替换为:
+
+```typescript
+async function handleLogout() {
+  const res = await uni.showModal({
+    title: '确认退出',
+    content: '确定要退出登录吗?',
+  })
+  if (res.confirm) {
+    uni.reLaunch({
+      url: '/pages/login/login',
+      complete: () => {
+        authStore.doLogout()
+      },
+    })
+  }
+}
+```
+
+- [x] **Step 5.2: Commit**
+
+```bash
+git add src/components/my/ProfileView.vue
+git commit -m "fix(logout): reLaunch first then clear auth state to avoid hang"
+```
+
+---
+
+## Task 6: 更新排班与车辆 API 类型
+
+**Files:**
+- Modify: `D:/work/绿水青山/清道夫App/src/api/schedule.ts`
+- Modify: `D:/work/绿水青山/清道夫App/src/api/vehicle.ts`
+- Create: `D:/work/绿水青山/清道夫App/src/api/team.ts`
+
+**Interfaces:**
+- Produces: `createSchedule(data: DispatchSchedule)` → `post('/schedule', data)`
+- Produces: `getVehicleSimpleList()` → `get('/vehicle/simple-list')`
+- Produces: `getTeamSimpleList()` → `get('/team/simple-list')`
+
+- [x] **Step 6.1: 定义 DispatchSchedule 类型**
+
+在 `src/api/schedule.ts` 顶部新增类型:
+
+```typescript
+export interface DispatchSchedule {
+  scheduleId?: number
+  teamId?: number
+  teamName?: string
+  workDate?: string
+  shiftType?: 'month_plan' | 'emergency' | string
+  vehicleIds?: string
+  staffIds?: string
+  status?: string
+  remark?: string
+}
+```
+
+- [x] **Step 6.2: 更新 createSchedule 签名**
+
+将 `createSchedule` 改为:
+
+```typescript
+export function createSchedule(data: DispatchSchedule) {
+  return post<number>('/schedule', data)
+}
+```
+
+- [x] **Step 6.3: 更新 getScheduleList 返回字段兼容**
+
+当前 `getScheduleList` 按 `year/month` 查询,后端 `/schedule/list` 是分页。需要改为按日期 `/schedule/date/{workDate}` 查询日排班。保留原函数用于月视图标记,新增:
+
+```typescript
+export function getSchedulesByDate(workDate: string) {
+  return get<DispatchSchedule[]>(`/schedule/date/${workDate}`)
+}
+```
+
+- [x] **Step 6.4: 更新 vehicle API**
+
+将 `src/api/vehicle.ts` 中 `getAvailableVehicles` 改为:
+
+```typescript
+export function getVehicleSimpleList() {
+  return get<Array<{ value: number; label: string }>>('/vehicle/simple-list')
+}
+```
+
+- [x] **Step 6.5: 创建 team API**
+
+创建 `src/api/team.ts`:
+
+```typescript
+import { get } from './request'
+
+export interface SimpleOption {
+  value: number
+  label: string
+}
+
+export function getTeamSimpleList() {
+  return get<SimpleOption[]>('/team/simple-list')
+}
+
+export function getTeamMembers(teamId: number) {
+  return get<Array<{ staffId: number; name: string }>>(`/team/${teamId}/members`)
+}
+```
+
+- [x] **Step 6.6: Commit**
+
+```bash
+git add src/api/schedule.ts src/api/vehicle.ts src/api/team.ts
+git commit -m "feat(api): align schedule/vehicle/team APIs with backend"
+```
+
+---
+
+## Task 7: 实现排班页新增排班弹窗
+
+**Files:**
+- Modify: `D:/work/绿水青山/清道夫App/src/components/dispatch/ScheduleView.vue`
+
+**Interfaces:**
+- Consumes: `getTeamSimpleList`, `getVehicleSimpleList`, `getTeamMembers`, `createSchedule`, `getSchedulesByDate`
+- Produces: 新增排班弹窗,提交后刷新列表
+
+- [x] **Step 7.1: 导入 API 与工具**
+
+在 `ScheduleView.vue` 的 `<script setup>` 顶部新增:
+
+```typescript
+import { getTeamSimpleList, getTeamMembers } from '@/api/team'
+import { getVehicleSimpleList } from '@/api/vehicle'
+import { getSchedulesByDate, createSchedule } from '@/api/schedule'
+```
+
+- [x] **Step 7.2: 新增弹窗状态与选项数据**
+
+在 `loading` 等响应式变量之后新增:
+
+```typescript
+const showModal = ref(false)
+const teamOptions = ref<Array<{ value: number; label: string }>>([])
+const vehicleOptions = ref<Array<{ value: number; label: string }>>([])
+const form = ref({
+  teamId: undefined as number | undefined,
+  teamName: '',
+  workDate: selectedDate.value,
+  shiftType: 'month_plan' as 'month_plan' | 'emergency',
+  vehicleIds: [] as number[],
+  staffIds: '' as string,
+  status: 'idle',
+  remark: '',
+})
+```
+
+- [x] **Step 7.3: 新增打开弹窗与数据加载方法**
+
+在 `addSchedule` 位置替换为:
+
+```typescript
+async function addSchedule() {
+  form.value.workDate = selectedDate.value
+  showModal.value = true
+  if (teamOptions.value.length === 0) {
+    try {
+      teamOptions.value = await getTeamSimpleList()
+    } catch (e) {
+      console.error('加载班组失败', e)
+    }
+  }
+  if (vehicleOptions.value.length === 0) {
+    try {
+      vehicleOptions.value = await getVehicleSimpleList()
+    } catch (e) {
+      console.error('加载车辆失败', e)
+    }
+  }
+}
+
+function closeModal() {
+  showModal.value = false
+}
+
+function resetForm() {
+  form.value = {
+    teamId: undefined,
+    teamName: '',
+    workDate: selectedDate.value,
+    shiftType: 'month_plan',
+    vehicleIds: [],
+    staffIds: '',
+    status: 'idle',
+    remark: '',
+  }
+}
+
+async function onTeamChange(teamId: number) {
+  const team = teamOptions.value.find(t => t.value === teamId)
+  form.value.teamName = team?.label || ''
+  try {
+    const members = await getTeamMembers(teamId)
+    form.value.staffIds = members.map(m => m.staffId).join(',')
+  } catch (e) {
+    console.error('加载班组成员失败', e)
+  }
+}
+
+async function submitSchedule() {
+  if (!form.value.teamId) {
+    uni.showToast({ title: '请选择班组', icon: 'none' })
+    return
+  }
+  if (!form.value.workDate) {
+    uni.showToast({ title: '请选择工作日期', icon: 'none' })
+    return
+  }
+  try {
+    await createSchedule({
+      teamId: form.value.teamId,
+      teamName: form.value.teamName,
+      workDate: form.value.workDate,
+      shiftType: form.value.shiftType,
+      vehicleIds: form.value.vehicleIds.join(','),
+      staffIds: form.value.staffIds,
+      status: form.value.status,
+      remark: form.value.remark,
+    })
+    uni.showToast({ title: '新增排班成功', icon: 'success' })
+    closeModal()
+    resetForm()
+    await fetchSchedules()
+  } catch (e) {
+    console.error('新增排班失败', e)
+  }
+}
+```
+
+- [x] **Step 7.4: 新增弹窗模板**
+
+在 `</view>`(app-container 结束)之前新增弹窗:
+
+```vue
+<uni-popup ref="popupRef" type="center" :is-mask-click="true" @change="onPopupChange">
+  <view class="bg-white rounded-2xl w-80 p-4">
+    <view class="flex justify-between items-center mb-4">
+      <text class="text-base font-semibold">新增排班</text>
+      <text class="uni-icons uniui-close text-gray-400" @click="closeModal"></text>
+    </view>
+
+    <view class="mb-3">
+      <text class="text-sm text-gray-600 block mb-1">选择班组</text>
+      <picker mode="selector" :range="teamOptions" range-key="label" :value="teamIndex" @change="(e) => { const idx = e.detail.value; form.teamId = teamOptions[idx].value; onTeamChange(form.teamId) }">
+        <view class="border border-gray-200 rounded-lg px-3 py-2 text-sm">
+          {{ teamNameDisplay || '请选择班组' }}
+        </view>
+      </picker>
+    </view>
+
+    <view class="mb-3">
+      <text class="text-sm text-gray-600 block mb-1">工作日期</text>
+      <picker mode="date" :value="form.workDate" @change="(e) => form.workDate = e.detail.value">
+        <view class="border border-gray-200 rounded-lg px-3 py-2 text-sm">
+          {{ form.workDate || '请选择日期' }}
+        </view>
+      </picker>
+    </view>
+
+    <view class="mb-3">
+      <text class="text-sm text-gray-600 block mb-1">排班类型</text>
+      <view class="flex rounded-lg border border-gray-200 overflow-hidden">
+        <view
+          class="flex-1 py-2 text-center text-sm"
+          :class="form.shiftType === 'month_plan' ? 'bg-primary text-white' : 'bg-white text-gray-600'"
+          @click="form.shiftType = 'month_plan'"
+        >月计划</view>
+        <view
+          class="flex-1 py-2 text-center text-sm"
+          :class="form.shiftType === 'emergency' ? 'bg-red-500 text-white' : 'bg-white text-gray-600'"
+          @click="form.shiftType = 'emergency'"
+        >应急</view>
+      </view>
+    </view>
+
+    <view class="mb-3">
+      <text class="text-sm text-gray-600 block mb-1">选择车辆</text>
+      <view class="flex flex-wrap gap-2">
+        <view
+          v-for="v in vehicleOptions"
+          :key="v.value"
+          class="px-2 py-1 rounded-lg text-xs border"
+          :class="form.vehicleIds.includes(v.value) ? 'bg-primary text-white border-primary' : 'bg-white text-gray-600 border-gray-200'"
+          @click="toggleVehicle(v.value)"
+        >
+          {{ v.label }}
+        </view>
+      </view>
+    </view>
+
+    <view class="mb-3">
+      <text class="text-sm text-gray-600 block mb-1">备注(可选)</text>
+      <textarea
+        v-model="form.remark"
+        class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm"
+        placeholder="请输入备注信息..."
+        :auto-height="true"
+      />
+    </view>
+
+    <view class="flex gap-3 mt-4">
+      <button class="flex-1 py-2 bg-white text-gray-600 rounded-lg border border-gray-200 text-sm" @click="closeModal">取消</button>
+      <button class="flex-1 py-2 bg-primary text-white rounded-lg text-sm" @click="submitSchedule">确认</button>
+    </view>
+  </view>
+</uni-popup>
+```
+
+- [x] **Step 7.5: 新增辅助 computed 与方法**
+
+在 `script setup` 中新增:
+
+```typescript
+const teamIndex = computed(() => teamOptions.value.findIndex(t => t.value === form.value.teamId))
+const teamNameDisplay = computed(() => teamOptions.value.find(t => t.value === form.value.teamId)?.label)
+
+function toggleVehicle(vehicleId: number) {
+  const idx = form.value.vehicleIds.indexOf(vehicleId)
+  if (idx >= 0) {
+    form.value.vehicleIds.splice(idx, 1)
+  } else {
+    form.value.vehicleIds.push(vehicleId)
+  }
+}
+
+function onPopupChange(e: { show: boolean }) {
+  if (!e.show) {
+    resetForm()
+  }
+}
+
+const popupRef = ref<any>(null)
+watch(showModal, (val) => {
+  if (val) {
+    popupRef.value?.open?.()
+  } else {
+    popupRef.value?.close?.()
+  }
+})
+```
+
+- [x] **Step 7.6: 更新 fetchSchedules 使用日期接口**
+
+将 `fetchSchedules` 改为按 selectedDate 查询:
+
+```typescript
+async function fetchSchedules() {
+  if (!selectedDate.value) return
+  loading.value = true
+  try {
+    const res = await getSchedulesByDate(selectedDate.value)
+    schedules.value = res || []
+  } catch (error) {
+    console.error('获取排班列表失败:', error)
+  } finally {
+    loading.value = false
+  }
+}
+```
+
+并修改 `watch` 为监听 `selectedDate`:
+
+```typescript
+watch(selectedDate, fetchSchedules)
+```
+
+- [x] **Step 7.7: 适配列表字段**
+
+当前 `daySchedules` 使用 `scheduleDate`、`taskName`、`staffName`、`vehicleNo`,但后端返回 `workDate`、`teamName`、`staffIds`、`vehicleIds`。修改列表渲染:
+
+```vue
+<uni-list-item
+  v-for="schedule in daySchedules"
+  :key="schedule.scheduleId"
+  :title="schedule.teamName || '未命名班组'"
+  :note="(schedule.staffIds || '未分配人员') + ' | ' + (schedule.vehicleIds || '未分配车辆')"
+  :right-text="getStatusText(schedule.status)"
+  show-arrow
+>
+```
+
+- [x] **Step 7.8: Commit**
+
+```bash
+git add src/components/dispatch/ScheduleView.vue
+git commit -m "feat(schedule): implement create schedule modal aligned with prototype"
+```
+
+---
+
+## Task 8: 构建验证与收尾
+
+**Files:**
+- All modified files
+
+- [x] **Step 8.1: 运行 type-check**
+
+Run: `npm run type-check`
+Expected: 无 TypeScript 错误
+
+- [x] **Step 8.2: 运行 mp-weixin 构建**
+
+Run: `npm run build:mp-weixin`
+Expected: 构建成功
+
+- [x] **Step 8.3: 更新 tasks.md 勾选状态**
+
+将 `openspec/changes/weapp-fixes-and-schedule-creation/tasks.md` 中对应任务全部勾选为 `[x]`。
+
+- [x] **Step 8.4: Commit tasks 更新**
+
+```bash
+git add openspec/changes/weapp-fixes-and-schedule-creation/tasks.md
+git commit -m "chore: mark all tasks complete"
+```
+
+---
+
+## 自审查
+
+- **Spec 覆盖**:新增排班能力覆盖原型弹窗字段、后端字段映射、保存与刷新;其余 4 项为既有实现修复。
+- **Placeholder 扫描**:无 TBD/TODO;所有代码块含完整实现。
+- **类型一致性**:`DispatchSchedule` 类型统一;`createSchedule` 接收该类型;车辆/班组下拉返回 `Array<{ value: number; label: string }>`。