---
archived-with: 2026-06-25-weapp-fixes-and-schedule-creation
status: final
---
# 清道夫 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 }>`。
