archived-with: 2026-06-25-weapp-fixes-and-schedule-creation
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
month_plan,应急 ↔ emergency。npm run type-check 与 npm run build:mp-weixin 通过。/vehicle/simple-list 接口Files:
D:/work/绿水青山/java/aquagreen-server/aquagreen-business/src/main/java/com/aquagreen/business/service/VehicleService.javaD:/work/绿水青山/java/aquagreen-server/aquagreen-business/src/main/java/com/aquagreen/business/controller/VehicleController.javaInterfaces:
Produces: GET /vehicle/simple-list → R<List<Map<String, Object>>> with { value: vehicleId, label: plateNo }
[x] Step 1.1: 在 VehicleService 添加 simpleList 方法
在 VehicleService.java 中 assignToTask 方法之后新增:
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;(如不存在)。
在 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;(如不存在)。
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"
Files:
D:/work/绿水青山/清道夫App/src/components/common/TopBar.vueD:/work/绿水青山/清道夫App/src/components/dispatch/ScheduleView.vue(可选,确认 showBack 传递)Interfaces:
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
}
)
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"
Files:
D:/work/绿水青山/清道夫App/src/components/task/TaskListView.vueInterfaces:
TopBar right slot 已包裹 CapsuleSafeArea当前 TaskListView.vue 中按钮已放入 TopBar right slot,但胶囊仍可能遮挡。原因:button 元素本身有 bg-primary 等样式,CapsuleSafeArea 仅给外层容器加了 margin-left;若按钮宽度或右侧 padding 导致溢出,需要调整。
将 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>
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"
Files:
D:/work/绿水青山/清道夫App/src/pages/home/home.vueD:/work/绿水青山/清道夫App/src/pages/task/task.vueD:/work/绿水青山/清道夫App/src/pages/schedule/schedule.vueD:/work/绿水青山/清道夫App/src/pages/my/my.vueD:/work/绿水青山/清道夫App/src/custom-tab-bar/index.jsInterfaces:
getTabBar() API from custom tabBar componentProduces: 切换 tab 后高亮即时同步
[x] Step 4.1: 在 custom-tab-bar/index.js 中暴露 setSelected 方法
在 custom-tab-bar/index.js 的 methods 对象中新增:
setSelected(index) {
this.setData({ selected: index })
}
在每个 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>
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"
Files:
D:/work/绿水青山/清道夫App/src/components/my/ProfileView.vueInterfaces:
uni.reLaunch, authStore.doLogoutProduces: 登出动画流畅
[x] Step 5.1: 调整 handleLogout 顺序
将 ProfileView.vue 中 handleLogout 函数替换为:
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"
Files:
D:/work/绿水青山/清道夫App/src/api/schedule.tsD:/work/绿水青山/清道夫App/src/api/vehicle.tsD:/work/绿水青山/清道夫App/src/api/team.tsInterfaces:
createSchedule(data: DispatchSchedule) → post('/schedule', data)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
}
将 createSchedule 改为:
export function createSchedule(data: DispatchSchedule) {
return post<number>('/schedule', data)
}
当前 getScheduleList 按 year/month 查询,后端 /schedule/list 是分页。需要改为按日期 /schedule/date/{workDate} 查询日排班。保留原函数用于月视图标记,新增:
export function getSchedulesByDate(workDate: string) {
return get<DispatchSchedule[]>(`/schedule/date/${workDate}`)
}
将 src/api/vehicle.ts 中 getAvailableVehicles 改为:
export function getVehicleSimpleList() {
return get<Array<{ value: number; label: string }>>('/vehicle/simple-list')
}
创建 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"
Files:
D:/work/绿水青山/清道夫App/src/components/dispatch/ScheduleView.vueInterfaces:
getTeamSimpleList, getVehicleSimpleList, getTeamMembers, createSchedule, getSchedulesByDateProduces: 新增排班弹窗,提交后刷新列表
[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'
在 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: '',
})
在 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)
}
}
在 </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>
在 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?.()
}
})
将 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)
当前 daySchedules 使用 scheduleDate、taskName、staffName、vehicleNo,但后端返回 workDate、teamName、staffIds、vehicleIds。修改列表渲染:
<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"
Files:
All modified files
[x] Step 8.1: 运行 type-check
Run: npm run type-check
Expected: 无 TypeScript 错误
Run: npm run build:mp-weixin
Expected: 构建成功
将 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"
DispatchSchedule 类型统一;createSchedule 接收该类型;车辆/班组下拉返回 Array<{ value: number; label: string }>。