Explorar o código

feat(schedule): support schedule detail editing and refine UI layout

- 排班详情弹窗改为可编辑,新增/保存共用同一弹窗
- 按月查询排班以正确标记日历排班点
- 日历选中样式与有排班标识优化
- 排班页/首页按钮移出胶囊遮挡区域
- 任务页移除 TopBar 左上角新增按钮,统一使用悬浮按钮

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
xuyunhui hai 1 semana
pai
achega
9f0c81bea9

+ 1 - 1
src/api/schedule.ts

@@ -28,7 +28,7 @@ export function createSchedule(data: DispatchSchedule) {
   return post<number>('/schedule', data)
 }
 
-export function updateSchedule(scheduleId: number, data: Partial<{ status: string; remark: string }>) {
+export function updateSchedule(scheduleId: number, data: DispatchSchedule) {
   return put(`/schedule/${scheduleId}`, data)
 }
 

+ 98 - 52
src/components/dispatch/ScheduleView.vue

@@ -1,43 +1,18 @@
 <template>
   <view class="app-container pb-20">
     <StatusBar v-if="showStatusBar" />
-    <TopBar v-if="showTopBar" title="排班管理" :show-back="showBack">
-      <template #right>
-        <view class="flex items-center gap-2">
-          <view class="text-sm text-blue-500 flex items-center" @click="goToToday">
-            <text class="uni-icons uniui-calendar-filled mr-1"></text>
-            今天
-          </view>
-          <view class="text-blue-500 text-sm flex items-center" @click="addSchedule">
-            <text class="uni-icons uniui-plusempty mr-1"></text>
-            新增排班
-          </view>
-        </view>
-      </template>
-    </TopBar>
+    <TopBar v-if="showTopBar" title="排班管理" :show-back="showBack" />
 
-    <!-- tab 页面专用操作栏 -->
+    <!-- tab 页面专用标题栏 -->
     <view v-if="!showTopBar" class="bg-white px-4 py-3 flex items-center justify-between border-b border-gray-100">
       <text class="text-base font-semibold text-gray-800">排班管理</text>
-      <CapsuleSafeArea>
-        <view class="flex items-center gap-3">
-          <view class="text-sm text-blue-500 flex items-center" @click="goToToday">
-            <text class="uni-icons uniui-calendar-filled mr-1"></text>
-            今天
-          </view>
-          <view class="text-blue-500 text-sm flex items-center" @click="addSchedule">
-            <text class="uni-icons uniui-plusempty mr-1"></text>
-            新增排班
-          </view>
-        </view>
-      </CapsuleSafeArea>
     </view>
 
     <!-- 月份切换 -->
-    <view class="bg-white px-4 py-3 flex items-center justify-between border-b border-gray-100">
+    <view class="bg-white px-4 py-3 flex items-center justify-center border-b border-gray-100">
       <view class="flex items-center">
         <view class="p-2" @click="changeMonth(-1)">
-          <text class="uni-icons uniui-arrowleft text-gray-600"></text>
+          <text class="uni-icons uniui-back text-gray-600"></text>
         </view>
         <text class="text-base font-semibold text-gray-800 mx-2">{{ currentYear }}年{{ currentMonth }}月</text>
         <view class="p-2" @click="changeMonth(1)">
@@ -46,6 +21,18 @@
       </view>
     </view>
 
+    <!-- 页面中部操作按钮 -->
+    <view class="bg-white px-4 py-2 flex items-center justify-center gap-6 border-b border-gray-100">
+      <view class="text-sm text-blue-500 flex items-center" @click="goToToday">
+        <text class="uni-icons uniui-calendar-filled mr-1"></text>
+        今天
+      </view>
+      <view class="text-blue-500 text-sm flex items-center" @click="addSchedule">
+        <text class="uni-icons uniui-plusempty mr-1"></text>
+        新增排班
+      </view>
+    </view>
+
     <!-- 日历 -->
     <view class="bg-white px-4 py-3">
       <!-- 星期标题 -->
@@ -60,28 +47,28 @@
         <view
           v-for="(date, dateIndex) in week"
           :key="dateIndex"
-          class="flex-1 h-11 flex flex-col items-center justify-center relative mx-1"
+          class="flex-1 h-14 flex flex-col items-center justify-center relative mx-1"
           :class="{
             'opacity-30': date.isOtherMonth,
-            'bg-blue-500 rounded-xl shadow-md': date.isToday && !isSelected(date),
-            'bg-white border-2 border-blue-500 rounded-xl shadow-sm': isSelected(date) && !date.isToday,
-            'bg-blue-600 rounded-xl shadow-md': isSelected(date) && date.isToday,
+            'bg-blue-50 rounded-xl': date.isToday && !isSelected(date),
+            'border-2 border-primary rounded-xl shadow-sm': isSelected(date) && !date.isToday,
+            'bg-primary rounded-xl shadow-md': isSelected(date) && date.isToday,
           }"
           @click="selectDate(date)"
         >
           <text
             class="text-sm font-medium"
             :class="{
-              'text-white': date.isToday || isSelected(date),
-              'text-gray-800': !date.isToday && !isSelected(date)
+              'text-primary': date.isToday && !isSelected(date),
+              'text-gray-800': !date.isToday || isSelected(date)
             }"
           >
             {{ date.day }}
           </text>
           <view
             v-if="date.hasSchedule"
-            class="w-1 h-1 rounded-full mt-1"
-            :class="date.isToday || isSelected(date) ? 'bg-white' : 'bg-blue-500'"
+            class="w-2 h-2 rounded-full mt-1"
+            :class="isSelected(date) ? 'bg-primary' : 'bg-primary'"
           ></view>
         </view>
       </view>
@@ -109,6 +96,8 @@
           :note="(schedule.staffIds || '未分配人员') + ' | ' + (schedule.vehicleIds || '未分配车辆')"
           :right-text="getStatusText(schedule.status)"
           show-arrow
+          clickable
+          @click="viewSchedule(schedule)"
         >
           <template #header>
             <view class="w-10 h-10 rounded-xl flex items-center justify-center mr-3" :class="getStatusBgClass(schedule.status)">
@@ -118,12 +107,12 @@
         </uni-list-item>
       </uni-list>
     </view>
-    <!-- 新增排班弹窗 -->
-    <view v-if="showModal" class="fixed inset-0 z-50 flex items-center justify-center">
+    <!-- 新增排班底部弹窗 -->
+    <view v-if="showModal" class="fixed inset-0 z-50 flex flex-col justify-end" style="padding-bottom: var(--window-bottom, 0);">
       <view class="absolute inset-0" style="background-color: rgba(0, 0, 0, 0.5);" @click="closeModal"></view>
-      <view class="relative bg-white rounded-2xl w-80 p-4 overflow-y-auto" style="max-height: 80vh;">
+      <view class="relative bg-white rounded-t-3xl w-full p-4 overflow-y-auto" style="max-height: 60vh; margin-bottom: 100rpx;">
         <view class="flex justify-between items-center mb-4">
-          <text class="text-base font-semibold text-gray-800">新增排班</text>
+          <text class="text-base font-semibold text-gray-800">{{ isDetail ? '排班详情' : '新增排班' }}</text>
           <text class="uni-icons uniui-close text-gray-400 p-2" @click="closeModal"></text>
         </view>
 
@@ -181,14 +170,14 @@
           <textarea
             v-model="form.remark"
             class="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm"
+            style="min-height: 40px;"
             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>
+          <button class="flex-1 py-2 bg-primary text-white rounded-lg text-sm" @click="submitSchedule">{{ isDetail ? '保存' : '确认' }}</button>
         </view>
       </view>
     </view>
@@ -199,11 +188,10 @@
 import { ref, computed, onMounted, watch } from 'vue'
 import StatusBar from '../common/StatusBar.vue'
 import TopBar from '../common/TopBar.vue'
-import CapsuleSafeArea from '../common/CapsuleSafeArea.vue'
 import EmptyState from '../common/EmptyState.vue'
 import UniList from '../uni-list/uni-list.vue'
 import UniListItem from '../uni-list/uni-list-item.vue'
-import { getSchedulesByDate, createSchedule } from '@/api/schedule'
+import { getSchedulesByDate, createSchedule, updateSchedule, getScheduleList } from '@/api/schedule'
 import { getTeamSimpleList, getTeamMembers } from '@/api/team'
 import { getVehicleSimpleList } from '@/api/vehicle'
 
@@ -227,6 +215,8 @@ const currentMonth = ref(new Date().getMonth() + 1)
 const selectedDate = ref('')
 
 const showModal = ref(false)
+const isDetail = ref(false)
+const currentScheduleId = ref<number | undefined>(undefined)
 const teamOptions = ref<Array<{ value: number; label: string }>>([])
 const vehicleOptions = ref<Array<{ value: number; label: string }>>([])
 
@@ -372,6 +362,9 @@ function toggleVehicle(vehicleId: number) {
 }
 
 async function addSchedule() {
+  isDetail.value = false
+  currentScheduleId.value = undefined
+  resetForm()
   form.value.workDate = selectedDate.value
   showModal.value = true
   if (teamOptions.value.length === 0) {
@@ -390,8 +383,44 @@ async function addSchedule() {
   }
 }
 
+function normalizeShiftType(shiftType: string | undefined): 'month_plan' | 'emergency' {
+  if (!shiftType) return 'month_plan'
+  if (shiftType === 'emergency' || shiftType.includes('应急')) return 'emergency'
+  return 'month_plan'
+}
+
+function viewSchedule(schedule: any) {
+  isDetail.value = true
+  currentScheduleId.value = schedule.scheduleId
+  form.value = {
+    teamId: schedule.teamId,
+    teamName: schedule.teamName || '',
+    workDate: schedule.workDate || '',
+    shiftType: normalizeShiftType(schedule.shiftType),
+    vehicleIds: parseVehicleIds(schedule.vehicleIds),
+    staffIds: schedule.staffIds || '',
+    status: schedule.status || 'idle',
+    remark: schedule.remark || '',
+  }
+  showModal.value = true
+  if (teamOptions.value.length === 0) {
+    getTeamSimpleList().then(res => { teamOptions.value = res }).catch(e => console.error('加载班组失败', e))
+  }
+  if (vehicleOptions.value.length === 0) {
+    getVehicleSimpleList().then(res => { vehicleOptions.value = res }).catch(e => console.error('加载车辆失败', e))
+  }
+}
+
+function parseVehicleIds(ids: string | number[] | undefined): number[] {
+  if (!ids) return []
+  if (Array.isArray(ids)) return ids.map(Number)
+  return ids.split(',').map(s => Number(s.trim())).filter(n => !isNaN(n))
+}
+
 function closeModal() {
   showModal.value = false
+  isDetail.value = false
+  currentScheduleId.value = undefined
 }
 
 function resetForm() {
@@ -428,7 +457,7 @@ async function submitSchedule() {
     return
   }
   try {
-    await createSchedule({
+    const payload = {
       teamId: form.value.teamId,
       teamName: form.value.teamName,
       workDate: form.value.workDate,
@@ -437,13 +466,19 @@ async function submitSchedule() {
       staffIds: form.value.staffIds,
       status: form.value.status,
       remark: form.value.remark,
-    })
-    uni.showToast({ title: '新增排班成功', icon: 'success' })
+    }
+    if (isDetail.value && currentScheduleId.value) {
+      await updateSchedule(currentScheduleId.value, payload)
+      uni.showToast({ title: '保存成功', icon: 'success' })
+    } else {
+      await createSchedule(payload)
+      uni.showToast({ title: '新增排班成功', icon: 'success' })
+    }
     closeModal()
     resetForm()
     await fetchSchedules()
   } catch (e) {
-    console.error('新增排班失败', e)
+    console.error(isDetail.value ? '保存排班失败' : '新增排班失败', e)
   }
 }
 
@@ -451,8 +486,19 @@ async function fetchSchedules() {
   if (!selectedDate.value) return
   loading.value = true
   try {
-    const res = await getSchedulesByDate(selectedDate.value)
-    schedules.value = res || []
+    const [dayRes, monthRes] = await Promise.all([
+      getSchedulesByDate(selectedDate.value),
+      getScheduleList(currentYear.value, currentMonth.value)
+    ])
+    const dayList = dayRes || []
+    const monthList = monthRes?.rows || monthRes || []
+    const allSchedules = [...dayList]
+    monthList.forEach((s: any) => {
+      if (!allSchedules.some((x: any) => x.scheduleId === s.scheduleId)) {
+        allSchedules.push(s)
+      }
+    })
+    schedules.value = allSchedules
   } catch (error) {
     console.error('获取排班列表失败:', error)
   } finally {
@@ -460,7 +506,7 @@ async function fetchSchedules() {
   }
 }
 
-watch(selectedDate, fetchSchedules)
+watch([selectedDate, currentMonth], fetchSchedules)
 
 onMounted(() => {
   const today = new Date().toISOString().split('T')[0]

+ 3 - 5
src/components/home/ConstructionHome.vue

@@ -10,13 +10,11 @@
             <text class="text-gray-500 text-xs block">工程一班</text>
             <text class="text-gray-800 text-lg font-bold">张师傅</text>
           </view>
+          <button class="ml-4 w-10 h-10 bg-surface rounded-full flex items-center justify-center relative" @click="goToNotification">
+            <text class="uni-icons uniui-notification-filled text-gray-500 text-lg"></text>
+          </button>
         </view>
       </template>
-      <template #right>
-        <button class="w-10 h-10 bg-surface rounded-full flex items-center justify-center relative" @click="goToNotification">
-          <text class="uni-icons uniui-notification-filled text-gray-500 text-lg"></text>
-        </button>
-      </template>
     </TopBar>
 
     <!-- 主内容区域 -->

+ 3 - 5
src/components/home/DispatchHome.vue

@@ -10,13 +10,11 @@
             <text class="text-gray-500 text-xs block">调度中心</text>
             <text class="text-gray-800 text-lg font-bold">控制台</text>
           </view>
+          <button class="ml-4 w-10 h-10 bg-surface rounded-full flex items-center justify-center relative" @click="goToNotification">
+            <text class="uni-icons uniui-notification-filled text-gray-500 text-lg"></text>
+          </button>
         </view>
       </template>
-      <template #right>
-        <button class="w-10 h-10 bg-surface rounded-full flex items-center justify-center relative" @click="goToNotification">
-          <text class="uni-icons uniui-notification-filled text-gray-500 text-lg"></text>
-        </button>
-      </template>
     </TopBar>
 
     <!-- 主内容区域 -->

+ 3 - 5
src/components/home/SalesHome.vue

@@ -11,13 +11,11 @@
             <text class="text-gray-500 text-xs block">下午好</text>
             <text class="text-gray-800 text-lg font-bold">{{ userName }}</text>
           </view>
+          <button class="ml-4 w-10 h-10 bg-surface rounded-full flex items-center justify-center relative" @click="goToNotification">
+            <text class="uni-icons uniui-notification-filled text-gray-500 text-lg"></text>
+          </button>
         </view>
       </template>
-      <template #right>
-        <button class="w-10 h-10 bg-surface rounded-full flex items-center justify-center relative" @click="goToNotification">
-          <text class="uni-icons uniui-notification-filled text-gray-500 text-lg"></text>
-        </button>
-      </template>
     </TopBar>
 
     <!-- 主内容区域 -->

+ 1 - 8
src/components/task/TaskListView.vue

@@ -3,14 +3,7 @@
     <TopBar :title="pageTitle">
       <template #right>
         <view class="flex items-center pr-2">
-          <button
-            v-if="config.publishPath"
-            class="px-3 py-1 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>