|
@@ -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 }>`。
|