|
|
@@ -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]
|