2026-06-24-weapp-fixes-and-schedule-creation.md 20 KB

清道夫 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-checknpm 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-listR<List<Map<String, Object>>> with { value: vehicleId, label: plateNo }

  • [x] Step 1.1: 在 VehicleService 添加 simpleList 方法

VehicleService.javaassignToTask 方法之后新增:

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;(如不存在)。

  • Step 1.2: 在 VehicleController 添加 /vehicle/simple-list 端点

VehicleController.java/vehicle/{vehicleId} 之前新增:

@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;(如不存在)。

  • Step 1.3: 后端编译验证

Run: cd D:/work/绿水青山/java/aquagreen-server && mvn compile -pl aquagreen-business -am Expected: BUILD SUCCESS

  • [x] Step 1.4: Commit

    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

const props = withDefaults(
  defineProps<{
    title: string
    showBack?: boolean
  }>(),
  {
    showBack: false
  }
)
  • Step 2.2: 全局搜索确认非 tab 页面已显式开启返回(如需要)

Run: grep -rn "<TopBar" src/ Expected: 非 tab 页面(如任务详情、发布页)若依赖返回,应已显式传入 :show-back="true";本次仅修复 tab 页面默认值问题,不扩大范围。

  • [x] Step 2.3: Commit

    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 导致溢出,需要调整。

  • Step 3.1: 为 right slot 按钮添加更明显的内边距控制

TaskListView.vue 中第 3-14 行替换为:

<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>
  • Step 3.2: 验证构建无 TS/模板错误

Run: npm run type-check Expected: 无错误

  • [x] Step 3.3: Commit

    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.jsmethods 对象中新增:

setSelected(index) {
  this.setData({ selected: index })
}
  • Step 4.2: 在 tab 页面 onShow 中同步高亮

在每个 tab 页面的 onShow 中添加(需从 @dcloudio/uni-app 导入 onShow):

src/pages/home/home.vue(索引 0):

import { onShow } from '@dcloudio/uni-app'

onShow(() => {
  const tabBar = getTabBar ? getTabBar() : null
  tabBar?.setSelected?.(0)
})

src/pages/task/task.vue(索引 1,commonTabs)或 2(dispatchTabs):

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):

<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):

<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>
  • 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

    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.vuehandleLogout 函数替换为:

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

    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 顶部新增类型:

export interface DispatchSchedule {
  scheduleId?: number
  teamId?: number
  teamName?: string
  workDate?: string
  shiftType?: 'month_plan' | 'emergency' | string
  vehicleIds?: string
  staffIds?: string
  status?: string
  remark?: string
}
  • Step 6.2: 更新 createSchedule 签名

createSchedule 改为:

export function createSchedule(data: DispatchSchedule) {
  return post<number>('/schedule', data)
}
  • Step 6.3: 更新 getScheduleList 返回字段兼容

当前 getScheduleListyear/month 查询,后端 /schedule/list 是分页。需要改为按日期 /schedule/date/{workDate} 查询日排班。保留原函数用于月视图标记,新增:

export function getSchedulesByDate(workDate: string) {
  return get<DispatchSchedule[]>(`/schedule/date/${workDate}`)
}
  • Step 6.4: 更新 vehicle API

src/api/vehicle.tsgetAvailableVehicles 改为:

export function getVehicleSimpleList() {
  return get<Array<{ value: number; label: string }>>('/vehicle/simple-list')
}
  • Step 6.5: 创建 team API

创建 src/api/team.ts

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

    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> 顶部新增:

import { getTeamSimpleList, getTeamMembers } from '@/api/team'
import { getVehicleSimpleList } from '@/api/vehicle'
import { getSchedulesByDate, createSchedule } from '@/api/schedule'
  • Step 7.2: 新增弹窗状态与选项数据

loading 等响应式变量之后新增:

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: '',
})
  • Step 7.3: 新增打开弹窗与数据加载方法

addSchedule 位置替换为:

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)
  }
}
  • Step 7.4: 新增弹窗模板

</view>(app-container 结束)之前新增弹窗:

<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>
  • Step 7.5: 新增辅助 computed 与方法

script setup 中新增:

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?.()
  }
})
  • Step 7.6: 更新 fetchSchedules 使用日期接口

fetchSchedules 改为按 selectedDate 查询:

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

watch(selectedDate, fetchSchedules)
  • Step 7.7: 适配列表字段

当前 daySchedules 使用 scheduleDatetaskNamestaffNamevehicleNo,但后端返回 workDateteamNamestaffIdsvehicleIds。修改列表渲染:

<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

    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 错误

  • Step 8.2: 运行 mp-weixin 构建

Run: npm run build:mp-weixin Expected: 构建成功

  • Step 8.3: 更新 tasks.md 勾选状态

openspec/changes/weapp-fixes-and-schedule-creation/tasks.md 中对应任务全部勾选为 [x]

  • [x] Step 8.4: Commit tasks 更新

    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 }>