Преглед изворни кода

feat(schedule): implement create schedule modal aligned with prototype

xuyunhui пре 1 недеља
родитељ
комит
2f534b0817
1 измењених фајлова са 186 додато и 10 уклоњено
  1. 186 10
      src/components/dispatch/ScheduleView.vue

+ 186 - 10
src/components/dispatch/ScheduleView.vue

@@ -88,8 +88,8 @@
         <uni-list-item
           v-for="schedule in daySchedules"
           :key="schedule.scheduleId"
-          :title="schedule.taskName"
-          :note="schedule.staffName + ' | ' + (schedule.vehicleNo || '未分配车辆')"
+          :title="schedule.teamName || '未命名班组'"
+          :note="(schedule.staffIds || '未分配人员') + ' | ' + (schedule.vehicleIds || '未分配车辆')"
           :right-text="getStatusText(schedule.status)"
           show-arrow
         >
@@ -101,6 +101,80 @@
         </uni-list-item>
       </uni-list>
     </view>
+    <!-- 新增排班弹窗 -->
+    <view v-if="showModal" class="fixed inset-0 z-50 flex items-center justify-center">
+      <view class="absolute inset-0 bg-black/50" @click="closeModal"></view>
+      <view class="relative bg-white rounded-2xl w-80 p-4 max-h-[80vh] overflow-y-auto">
+        <view class="flex justify-between items-center mb-4">
+          <text class="text-base font-semibold text-gray-800">新增排班</text>
+          <text class="uni-icons uniui-close text-gray-400 p-2" @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: any) => { 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 text-gray-800">
+              {{ 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: any) => form.workDate = e.detail.value">
+            <view class="border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-800">
+              {{ 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>
+    </view>
   </view>
 </template>
 
@@ -111,7 +185,9 @@ import TopBar from '../common/TopBar.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 { getScheduleList } from '@/api/schedule'
+import { getSchedulesByDate, createSchedule } from '@/api/schedule'
+import { getTeamSimpleList, getTeamMembers } from '@/api/team'
+import { getVehicleSimpleList } from '@/api/vehicle'
 
 withDefaults(
   defineProps<{
@@ -132,6 +208,24 @@ const currentYear = ref(new Date().getFullYear())
 const currentMonth = ref(new Date().getMonth() + 1)
 const selectedDate = ref('')
 
+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: '',
+  shiftType: 'month_plan' as 'month_plan' | 'emergency',
+  vehicleIds: [] as number[],
+  staffIds: '',
+  status: 'idle',
+  remark: '',
+})
+
+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)
+
 const weekDays = ['日', '一', '二', '三', '四', '五', '六']
 
 const calendarWeeks = computed(() => {
@@ -158,7 +252,7 @@ const calendarWeeks = computed(() => {
   for (let day = 1; day <= daysInMonth; day++) {
     const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
     const isToday = dateStr === new Date().toISOString().split('T')[0]
-    const hasSchedule = schedules.value.some(s => s.scheduleDate === dateStr)
+    const hasSchedule = schedules.value.some(s => s.workDate === dateStr)
 
     currentWeek.push({
       day,
@@ -191,7 +285,7 @@ const calendarWeeks = computed(() => {
 
 const daySchedules = computed(() => {
   if (!selectedDate.value) return []
-  return schedules.value.filter(s => s.scheduleDate === selectedDate.value)
+  return schedules.value.filter(s => s.workDate === selectedDate.value)
 })
 
 function changeMonth(delta: number) {
@@ -250,15 +344,97 @@ function getStatusIconClass(status: string) {
   return map[status] || 'uniui-calendar text-gray-400'
 }
 
-function addSchedule() {
-  uni.showToast({ title: '排班功能开发中', icon: 'none' })
+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)
+  }
+}
+
+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)
+  }
 }
 
 async function fetchSchedules() {
+  if (!selectedDate.value) return
   loading.value = true
   try {
-    const res = await getScheduleList(currentYear.value, currentMonth.value)
-    schedules.value = res.rows || []
+    const res = await getSchedulesByDate(selectedDate.value)
+    schedules.value = res || []
   } catch (error) {
     console.error('获取排班列表失败:', error)
   } finally {
@@ -266,7 +442,7 @@ async function fetchSchedules() {
   }
 }
 
-watch(() => [currentYear.value, currentMonth.value], fetchSchedules)
+watch(selectedDate, fetchSchedules)
 
 onMounted(() => {
   const today = new Date().toISOString().split('T')[0]