|
|
@@ -0,0 +1,276 @@
|
|
|
+<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>
|
|
|
+
|
|
|
+ <!-- 月份切换 -->
|
|
|
+ <view class="bg-white px-4 py-3 flex items-center justify-between 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>
|
|
|
+ </view>
|
|
|
+ <text class="text-base font-semibold text-gray-800 mx-2">{{ currentYear }}年{{ currentMonth }}月</text>
|
|
|
+ <view class="p-2" @click="changeMonth(1)">
|
|
|
+ <text class="uni-icons uniui-arrowright text-gray-600"></text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 日历 -->
|
|
|
+ <view class="bg-white px-4 py-3">
|
|
|
+ <!-- 星期标题 -->
|
|
|
+ <view class="flex mb-2">
|
|
|
+ <view v-for="day in weekDays" :key="day" class="flex-1 text-center">
|
|
|
+ <text class="text-xs text-gray-400">{{ day }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 日期 -->
|
|
|
+ <view v-for="(week, weekIndex) in calendarWeeks" :key="weekIndex" class="flex mb-2">
|
|
|
+ <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="{
|
|
|
+ '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,
|
|
|
+ }"
|
|
|
+ @click="selectDate(date)"
|
|
|
+ >
|
|
|
+ <text
|
|
|
+ class="text-sm font-medium"
|
|
|
+ :class="{
|
|
|
+ 'text-white': 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'"
|
|
|
+ ></view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 排班列表 -->
|
|
|
+ <view class="px-4 mt-4">
|
|
|
+ <view class="flex justify-between items-center mb-3">
|
|
|
+ <text class="text-base font-semibold text-gray-800">{{ selectedDate }} 排班</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="loading" class="py-10 text-center">
|
|
|
+ <text class="text-gray-400">加载中...</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-else-if="daySchedules.length === 0">
|
|
|
+ <EmptyState message="该日无排班" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <uni-list v-else border>
|
|
|
+ <uni-list-item
|
|
|
+ v-for="schedule in daySchedules"
|
|
|
+ :key="schedule.scheduleId"
|
|
|
+ :title="schedule.taskName"
|
|
|
+ :note="schedule.staffName + ' | ' + (schedule.vehicleNo || '未分配车辆')"
|
|
|
+ :right-text="getStatusText(schedule.status)"
|
|
|
+ show-arrow
|
|
|
+ >
|
|
|
+ <template #header>
|
|
|
+ <view class="w-10 h-10 rounded-xl flex items-center justify-center mr-3" :class="getStatusBgClass(schedule.status)">
|
|
|
+ <text class="uni-icons text-lg" :class="getStatusIconClass(schedule.status)"></text>
|
|
|
+ </view>
|
|
|
+ </template>
|
|
|
+ </uni-list-item>
|
|
|
+ </uni-list>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, computed, onMounted, watch } from 'vue'
|
|
|
+import StatusBar from '../common/StatusBar.vue'
|
|
|
+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'
|
|
|
+
|
|
|
+withDefaults(
|
|
|
+ defineProps<{
|
|
|
+ showTopBar?: boolean
|
|
|
+ showBack?: boolean
|
|
|
+ showStatusBar?: boolean
|
|
|
+ }>(),
|
|
|
+ {
|
|
|
+ showTopBar: true,
|
|
|
+ showBack: true,
|
|
|
+ showStatusBar: false,
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
+const loading = ref(false)
|
|
|
+const schedules = ref<any[]>([])
|
|
|
+const currentYear = ref(new Date().getFullYear())
|
|
|
+const currentMonth = ref(new Date().getMonth() + 1)
|
|
|
+const selectedDate = ref('')
|
|
|
+
|
|
|
+const weekDays = ['日', '一', '二', '三', '四', '五', '六']
|
|
|
+
|
|
|
+const calendarWeeks = computed(() => {
|
|
|
+ const year = currentYear.value
|
|
|
+ const month = currentMonth.value
|
|
|
+ const firstDay = new Date(year, month - 1, 1)
|
|
|
+ const lastDay = new Date(year, month, 0)
|
|
|
+ const firstWeekDay = firstDay.getDay()
|
|
|
+ const daysInMonth = lastDay.getDate()
|
|
|
+
|
|
|
+ const weeks: any[] = []
|
|
|
+ let currentWeek: any[] = []
|
|
|
+
|
|
|
+ const prevMonthDays = new Date(year, month - 1, 0).getDate()
|
|
|
+ for (let i = firstWeekDay - 1; i >= 0; i--) {
|
|
|
+ currentWeek.push({
|
|
|
+ day: prevMonthDays - i,
|
|
|
+ isOtherMonth: true,
|
|
|
+ isToday: false,
|
|
|
+ hasSchedule: false,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ 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)
|
|
|
+
|
|
|
+ currentWeek.push({
|
|
|
+ day,
|
|
|
+ isOtherMonth: false,
|
|
|
+ isToday,
|
|
|
+ hasSchedule,
|
|
|
+ dateStr,
|
|
|
+ })
|
|
|
+
|
|
|
+ if (currentWeek.length === 7) {
|
|
|
+ weeks.push(currentWeek)
|
|
|
+ currentWeek = []
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (currentWeek.length > 0) {
|
|
|
+ for (let i = 1; currentWeek.length < 7; i++) {
|
|
|
+ currentWeek.push({
|
|
|
+ day: i,
|
|
|
+ isOtherMonth: true,
|
|
|
+ isToday: false,
|
|
|
+ hasSchedule: false,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ weeks.push(currentWeek)
|
|
|
+ }
|
|
|
+
|
|
|
+ return weeks
|
|
|
+})
|
|
|
+
|
|
|
+const daySchedules = computed(() => {
|
|
|
+ if (!selectedDate.value) return []
|
|
|
+ return schedules.value.filter(s => s.scheduleDate === selectedDate.value)
|
|
|
+})
|
|
|
+
|
|
|
+function changeMonth(delta: number) {
|
|
|
+ let newMonth = currentMonth.value + delta
|
|
|
+ if (newMonth > 12) {
|
|
|
+ newMonth = 1
|
|
|
+ currentYear.value++
|
|
|
+ } else if (newMonth < 1) {
|
|
|
+ newMonth = 12
|
|
|
+ currentYear.value--
|
|
|
+ }
|
|
|
+ currentMonth.value = newMonth
|
|
|
+}
|
|
|
+
|
|
|
+function goToToday() {
|
|
|
+ const now = new Date()
|
|
|
+ currentYear.value = now.getFullYear()
|
|
|
+ currentMonth.value = now.getMonth() + 1
|
|
|
+ selectedDate.value = now.toISOString().split('T')[0]
|
|
|
+}
|
|
|
+
|
|
|
+function selectDate(date: any) {
|
|
|
+ if (!date.isOtherMonth) {
|
|
|
+ selectedDate.value = date.dateStr
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function isSelected(date: any): boolean {
|
|
|
+ return date.dateStr === selectedDate.value
|
|
|
+}
|
|
|
+
|
|
|
+function getStatusText(status: string) {
|
|
|
+ const map: Record<string, string> = {
|
|
|
+ scheduled: '已排班',
|
|
|
+ completed: '已完成',
|
|
|
+ cancelled: '已取消',
|
|
|
+ }
|
|
|
+ return map[status] || status
|
|
|
+}
|
|
|
+
|
|
|
+function getStatusBgClass(status: string) {
|
|
|
+ const map: Record<string, string> = {
|
|
|
+ scheduled: 'bg-blue-50',
|
|
|
+ completed: 'bg-green-50',
|
|
|
+ cancelled: 'bg-red-50',
|
|
|
+ }
|
|
|
+ return map[status] || 'bg-gray-50'
|
|
|
+}
|
|
|
+
|
|
|
+function getStatusIconClass(status: string) {
|
|
|
+ const map: Record<string, string> = {
|
|
|
+ scheduled: 'uniui-calendar-filled text-blue-500',
|
|
|
+ completed: 'uniui-checkmarkempty text-green-500',
|
|
|
+ cancelled: 'uniui-close text-red-500',
|
|
|
+ }
|
|
|
+ return map[status] || 'uniui-calendar text-gray-400'
|
|
|
+}
|
|
|
+
|
|
|
+function addSchedule() {
|
|
|
+ uni.showToast({ title: '排班功能开发中', icon: 'none' })
|
|
|
+}
|
|
|
+
|
|
|
+async function fetchSchedules() {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const res = await getScheduleList(currentYear.value, currentMonth.value)
|
|
|
+ schedules.value = res.rows || []
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取排班列表失败:', error)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+watch(() => [currentYear.value, currentMonth.value], fetchSchedules)
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ const today = new Date().toISOString().split('T')[0]
|
|
|
+ selectedDate.value = today
|
|
|
+ fetchSchedules()
|
|
|
+})
|
|
|
+</script>
|