2025-08-20 11:48:20 +08:00

865 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- 排班表 -->
<template>
<view class="main" :style="{ paddingTop: (menu.bottom + 28) + 'px' }">
<view class="summaryTab" :style="{ height: (menu.bottom + 14) + 'px' }">
<view class="leftArrow" :style="{ top: (menu.top + ((menu.height - 24) / 2)) + 'px' }">
<image class="img" src="https://eshangtech.com/ShopICO/ahyd-BID/commercial/navigation-left.svg"
@click="handleBack"></image>
<view class="picker" :style="{ top: (menu.bottom + 24) + 'px' }" @click="handleChangeService">
<view class="selectService">
<image class="img" src="https://eshangtech.com/ShopICO/ahyd-BID/commercial/fixed.svg"></image>
<view class="select">
<view class="content">
<view class="uni-input">{{ serviceInfo.SERVERPART_NAME ? serviceInfo.SERVERPART_NAME :
'' }}
</view>
<p class="area">{{ serviceInfo.SPREGIONTYPE_NAME ? serviceInfo.SPREGIONTYPE_NAME : '' }}
</p>
<image class="rightArrow"
src="https://eshangtech.com/ShopICO/ahyd-BID/commercial/rightArrow.svg"></image>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="rosterContent">
<!-- 月份和员工选择器 -->
<view class="selectorRow">
<view class="dateBox">
<view class="centerDateBox">
<view class="arrowBtn" @tap="handleChangeDate(2)">
<image class="arrowIcon" src="https://eshangtech.com/cyy_DIB/leftArrowIcon.png" />
</view>
<view class="center">
<text class="date-text">{{ selectDate ? $util.cutDate(new Date(selectDate), 'YYYY-MM') :
""
}}</text>
</view>
<view class="arrowBtn" @tap="handleChangeDate(1)">
<image class="arrowIcon" src="https://eshangtech.com/cyy_DIB/rightArrowIcon.png" />
</view>
</view>
</view>
<!-- 员工选择器 -->
<view class="employeeSelector">
<picker @change="handleEmployeeChange" :value="selectedEmployeeIndex" :range="userList"
:range-key="'userName'">
<view class="employeePickerBox">
<view class="selectedEmployee">{{ userList[selectedEmployeeIndex] ?
userList[selectedEmployeeIndex].userName :
'请选择员工' }}
</view>
<image class="rightArrow"
src="https://eshangtech.com/ShopICO/ahyd-BID/commercial/rightArrow.svg"></image>
</view>
</picker>
</view>
</view>
<!-- 员工信息 -->
<view v-if="selectedEmployee" class="employeeDetail">
<view class="employeeCard">
<view class="employeeInfo">
<view class="avatar">{{ getFirstChar(selectedEmployee.userName) }}</view>
<view class="basicInfo">
<view class="nameRow">
<image class="personIcon" src="https://eshangtech.com/cyy_DIB/personIcon.png"></image>
<view class="employeeName">{{ selectedEmployee.userName || '-' }}</view>
</view>
<view class="phoneRow">
<image class="phoneIcon" src="https://eshangtech.com/cyy_DIB/phoneLabelIcon.png">
</image>
<view class="employeePhone">{{ selectedEmployee.phone || '-' }}</view>
</view>
</view>
<view class="jobInfo">
<view class="employeeJob" v-if="selectedEmployee.psnJob">{{ selectedEmployee.psnJob }}
</view>
</view>
</view>
</view>
</view>
<!-- 空状态提示 - 只有在已加载完成且真的没有数据时才显示 -->
<view v-if="!loading && hasLoaded && userList.length === 0" class="emptyState">
<view class="emptyIcon">📋</view>
<view class="emptyText">暂无员工数据</view>
<view class="emptyDesc">请检查服务区设置或联系管理员</view>
<button class="retryBtn" @tap="retryLoad" type="primary" size="mini">重新加载</button>
</view>
<!-- 员工无排班数据提示 - 只有在已加载完成且真的没有数据时才显示 -->
<view v-if="!loading && hasLoaded && userList.length > 0 && !selectedEmployee" class="emptySchedule">
<view class="emptyIcon">📅</view>
<view class="emptyText">暂无排班数据</view>
<view class="emptyDesc">该员工在当前月份暂无排班安排</view>
</view>
<!-- 日历格式排班表 -->
<view v-if="selectedEmployee" class="calendarRoster">
<view class="weekHeader">
<view v-for="(weekName, index) in weekNames" :key="index" class="weekHeaderItem"
:class="{ 'weekend': index === 0 || index === 6 }">
{{ weekName }}
</view>
</view>
<view class="calendarGrid">
<view v-for="(n, index) in firstDayOfWeek" :key="index" class="calendarCell empty"></view>
<view v-for="day in monthDays" :key="day.date" class="calendarCell" :class="{
'weekend': day.isWeekend,
'today': day.isToday
}">
<view class="dateNumber">{{ day.day }}</view>
<view class="scheduleInfo" :class="{
'work': selectedEmployee && selectedEmployee[`day${day.day}`] && !selectedEmployee[`day${day.day}`].toString().toLowerCase().includes('休'),
'rest': selectedEmployee && selectedEmployee[`day${day.day}`] && selectedEmployee[`day${day.day}`].toString().toLowerCase().includes('休'),
'empty': !selectedEmployee || !selectedEmployee[`day${day.day}`]
}">
{{ selectedEmployee && selectedEmployee[`day${day.day}`] ?
(selectedEmployee[`day${day.day}`].toString().toLowerCase().includes('休') ? '休' : '班') : ''
}}
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
const now = new Date()
const currentYear = now.getFullYear()
const currentMonthIndex = now.getMonth()
const nowDay = this.$util.cutDate(new Date(), 'YYYY-MM-DD') // 有数据的最近日期
return {
menu: {},
selectDate: nowDay,// 选中的时间
serviceInfo: {},
currentYear: currentYear,
currentMonth: currentMonthIndex,
selectedEmployeeIndex: 0,
selectedEmployee: null,
weekNames: ['日', '一', '二', '三', '四', '五', '六'],
// 模拟排班数据,实际使用时从接口获取
rosterData: [],
userList: [],// 员工的数组
selectUser: "",//当前选择的员工
isFirst: true,
monthDays: [],
firstDayOfWeek: "",
loading: false,
retryCount: 0,
hasLoaded: false, // 是否已经完成过初始加载
}
},
computed: {
// 获取月份第一天是星期几 (0=周日, 1=周一, ..., 6=周六)
// firstDayOfWeek() {
// const firstDay = new Date(this.currentYear, this.currentMonth, 1)
// return firstDay.getDay()
// }
},
onLoad() {
this.menu = uni.getMenuButtonBoundingClientRect()
let currentService = uni.getStorageSync('currentService')
this.serviceInfo = currentService
this.handleGetNewMonthData()
// 加载排班数据
this.loadRosterData(currentService.SERVERPART_NAME)
},
onShow() {
let currentService = uni.getStorageSync('currentService')
console.log('currentService', currentService);
// 防护:检查 currentService 是否有效
if (!currentService || (!currentService.Serverpart_ID && !currentService.SERVERPART_ID)) {
console.warn('currentService 为空或无效,跳过更新');
this.isFirst = false;
return;
}
// 获取统一的服务区ID
const currentServiceId = currentService.Serverpart_ID || currentService.SERVERPART_ID;
const currentServiceName = currentService.SERVERPART_NAME;
// 只有当服务区真正发生变化且不是首次加载时才更新
if (currentServiceId && currentServiceId !== this.serviceInfo.SERVERPART_ID && !this.isFirst) {
this.serviceInfo = currentService
this.rosterData = []
this.userList = []
this.selectUser = null
this.loadRosterData(currentServiceName)
} else {
this.isFirst = false;
}
},
methods: {
// 获取姓名首字符
getFirstChar(name) {
return name ? name.charAt(0).toUpperCase() : '?'
},
// 重试加载
retryLoad() {
if (this.retryCount < 3) {
this.retryCount++
this.loadRosterData(this.serviceInfo.SERVERPART_NAME)
} else {
uni.showToast({
title: '多次重试失败,请检查网络连接',
icon: 'none'
})
}
},
handleGetNewMonthData() {
const year = this.currentYear
const month = this.currentMonth
const daysInMonth = new Date(year, month + 1, 0).getDate()
const today = new Date()
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
const days = []
const weekTexts = ['日', '一', '二', '三', '四', '五', '六']
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month, day)
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`
const weekDay = date.getDay()
days.push({
day: day,
date: dateStr,
weekDay: weekDay,
weekText: weekTexts[weekDay],
isWeekend: weekDay === 0 || weekDay === 6,
isToday: dateStr === todayStr
})
}
this.monthDays = days
const firstDay = new Date(this.currentYear, this.currentMonth, 1)
this.firstDayOfWeek = firstDay.getDay()
},
handleChangeService() {
this.$util.toNextRoute("navigateTo", "/pages/map/index?type=attendanceStatus");
},
// 修改日期
async handleChangeDate(type) {
// type 1 加一天 2 减一天
// 兼容 iOS把 2025-08-15 转成 2025/08/15
const cur = new Date((this.selectDate || '').replace(/-/g, '/'));
if (Number.isNaN(cur.getTime())) return;
// 1加一月2减一月其他不变
const delta = type === 1 ? 1 : (type === 2 ? -1 : 0);
// 目标月份的最后一天new Date(年, 目标月+1, 0)
const last = new Date(cur.getFullYear(), cur.getMonth() + delta + 1, 0);
const y = last.getFullYear();
const m = String(last.getMonth() + 1).padStart(2, '0');
const d = String(last.getDate()).padStart(2, '0');
this.selectDate = `${y}-${m}-${d}`;
// 修复currentMonth应该是数字类型0-11而不是字符串
this.currentYear = y
this.currentMonth = last.getMonth() // 使用数字类型的月份索引
this.handleGetNewMonthData()
this.retryCount = 0 // 重置重试次数
// 月份切换时不清空selectedEmployee保持显示直到新数据加载完成
await this.handleGetCurrentUserScheduleInfo()
},
// 返回上一页
handleBack() {
uni.navigateBack()
},
// 员工选择变化
handleEmployeeChange(e) {
const selectedIndex = Number(e.detail.value)
if (selectedIndex >= 0 && selectedIndex < this.userList.length) {
this.selectedEmployeeIndex = selectedIndex
this.selectUser = this.userList[selectedIndex]
// 切换员工时,暂时保持当前状态,直到新数据加载完成
this.handleGetCurrentUserScheduleInfo()
}
},
// 加载排班数据
async loadRosterData(SERVERPART_NAME) {
if (!SERVERPART_NAME) {
uni.showToast({
title: '请先选择服务区',
icon: 'none'
})
return
}
this.loading = true
let req = {
bsessionKey: "0B30475A94674D608022885F7763959B",
workTime: new Date(this.selectDate).getTime(),
saName: SERVERPART_NAME,
phone: "",
}
uni.showLoading({
title: "加载中..."
})
try {
const data = await new Promise((resolve, reject) => {
uni.request({
url: "https://fwqznxj.yciccloud.com:9081/ynjt/pushManage/queryUserSchedule",
method: "POST",
data: req,
header: {
"content-type": "application/x-www-form-urlencoded",
},
success(res) {
if (res.data && res.data.data) {
resolve(res.data.data)
} else {
reject(new Error('数据格式错误'))
}
},
fail(err) {
reject(err)
}
});
});
const userList = Array.isArray(data) ? data : []
this.userList = userList
this.selectedEmployeeIndex = 0
this.selectUser = userList.length > 0 ? userList[0] : null
if (userList.length > 0) {
await this.handleGetCurrentUserScheduleInfo()
} else {
this.selectedEmployee = null
uni.showToast({
title: '当前服务区暂无员工',
icon: 'none'
})
}
// 标记已完成初始加载
this.hasLoaded = true
} catch (error) {
console.error('加载排班数据失败:', error)
this.userList = []
this.selectedEmployee = null
uni.showToast({
title: error.message || '加载失败,请重试',
icon: 'none'
})
} finally {
this.loading = false
uni.hideLoading()
}
},
// 拿到当前用户的排班信息
async handleGetCurrentUserScheduleInfo() {
if (!this.selectUser || !this.selectUser.phone) {
// 只有在已经完成过初始加载后才显示空状态
if (this.hasLoaded) {
this.selectedEmployee = null
}
return
}
let req = {
bsessionKey: "20ED2B50179D4877853C4DED45D8179E",
date: new Date(this.selectDate).getTime(),
phone: this.selectUser.phone
}
try {
const data = await new Promise((resolve, reject) => {
uni.request({
url: "https://fwqznxj.yciccloud.com:9081/ynjt/pushManage/queryWorkDetails",
method: "POST",
data: req,
header: {
"content-type": "application/x-www-form-urlencoded",
},
success(res) {
if (res.data && res.data.data && Array.isArray(res.data.data) && res.data.data.length > 0) {
resolve(res.data.data)
} else {
resolve(null)
}
},
fail(err) {
reject(err)
}
});
});
if (data && data.length > 0) {
this.selectedEmployee = data[0]
} else {
this.selectedEmployee = null
// 只有在已经加载过的情况下才显示提示
if (this.hasLoaded) {
uni.showToast({
title: '该员工暂无排班信息',
icon: 'none'
})
}
}
// 标记已完成加载
this.hasLoaded = true
} catch (error) {
console.error('获取排班信息失败:', error)
this.selectedEmployee = null
uni.showToast({
title: '获取排班信息失败',
icon: 'none'
})
}
}
}
}
</script>
<style lang="less" scoped>
@bg: #f8f9fa;
@muted: #666;
@card: #fff;
@shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
@primary: #27B25F;
@primary2: #4CCC7F;
@ok: #2ed573;
@warn: #ff9f43;
@danger: #ff4757;
.main {
width: 100vw;
min-height: 100vh;
background-color: #f0f2f3;
box-sizing: border-box;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
padding: 16rpx 32rpx 0;
.summaryTab {
position: fixed;
left: 0;
top: 0;
width: 100vw;
background-image: url("https://eshangtech.com/minTestImg/pageBg.png");
z-index: 99;
display: flex;
align-items: center;
box-sizing: border-box;
padding-bottom: 16px;
.leftArrow {
width: 100%;
height: 24px;
position: absolute;
z-index: 99999999999;
box-sizing: border-box;
display: flex;
align-items: center;
padding: 0 32rpx;
.img {
width: 24px;
height: 24px;
margin-right: 8px;
z-index: 99;
}
.picker {
.selectService {
display: flex;
align-items: center;
.img {
width: 40px;
height: 40px;
z-index: 2;
}
.select {
height: 32px;
background: #fff;
border-radius: 0 16px 16px 0;
transform: translateX(-40px);
box-sizing: border-box;
padding-left: 35px;
padding-right: 8rpx;
display: flex;
align-items: center;
.content {
display: flex;
align-items: center;
.uni-input {
padding: 0;
background: transparent;
font-size: 28rpx;
font-family: PingFangSC-Semibold, PingFang SC;
font-weight: 600;
color: #160002;
}
.area {
font-size: 12px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #786B6C;
line-height: 40px;
margin-left: 4px;
}
.rightArrow {
width: 12px;
height: 12px;
}
}
}
}
}
}
}
.rosterContent {
width: 100%;
background-color: #fff;
box-sizing: border-box;
padding: 0 24rpx 8rpx;
border-radius: 8rpx;
/* 选择器行布局 */
.selectorRow {
display: flex;
gap: 24rpx;
padding: 24rpx 0;
align-items: center;
justify-content: space-between;
}
.dateBox {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
.centerDateBox {
display: flex;
align-items: center;
gap: 10rpx;
border-radius: 30rpx;
background-color: #fff;
// border: 2rpx solid #27B35F;
// background: rgba(255, 255, 255, .15);
.arrowBtn {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
border-radius: 50%;
// background: rgba(39, 178, 95, 0.1);
.arrowIcon {
width: 40rpx;
height: 40rpx;
}
}
.center {
.date-text {
font-size: 28rpx;
font-weight: 700;
}
}
}
}
/* 员工选择器 */
.employeeSelector {
.employeePickerBox {
display: flex;
align-items: center;
justify-content: space-between;
// padding: 20rpx 24rpx;
// background: #f8f9fa;
border-radius: 30rpx;
// border: 2rpx solid #e0e0e0;
.selectedEmployee {
font-size: 24rpx;
font-weight: 400;
color: #000;
margin-right: 8rpx;
}
.rightArrow {
width: 26rpx;
height: 26rpx;
}
}
}
/* 员工信息卡片 */
.employeeDetail {
margin-bottom: 32rpx;
.employeeCard {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
box-shadow: @shadow;
border: 1px solid rgba(39, 178, 95, 0.08);
.employeeInfo {
display: flex;
align-items: flex-start;
justify-content: space-between;
.avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: @primary;
color: #fff;
font-size: 24rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
flex-shrink: 0;
}
.basicInfo {
flex: 1;
display: flex;
flex-direction: column;
gap: 12rpx;
margin-top: 4rpx;
.nameRow {
display: flex;
align-items: center;
.personIcon {
width: 24rpx;
height: 24rpx;
margin-right: 8rpx;
}
.employeeName {
font-size: 28rpx;
font-weight: 600;
color: #2c3e50;
line-height: 1.3;
}
}
.phoneRow {
display: flex;
align-items: center;
.phoneIcon {
width: 24rpx;
height: 24rpx;
margin-right: 8rpx;
}
.employeePhone {
font-size: 24rpx;
color: #666;
line-height: 1.3;
}
}
}
.jobInfo {
display: flex;
align-items: flex-start;
margin-top: 4rpx;
.employeeJob {
font-size: 22rpx;
color: @primary;
background: rgba(39, 178, 95, 0.1);
padding: 6rpx 12rpx;
border-radius: 16rpx;
font-weight: 500;
}
}
}
}
}
/* 日历格式排班表 */
.calendarRoster {
background: #fff;
border-radius: 12rpx;
// padding: 32rpx;
box-shadow: @shadow;
margin-bottom: 32rpx;
/* 星期标题 */
.weekHeader {
display: flex;
border-bottom: 2rpx solid #e0e0e0;
// margin-bottom: 16rpx;
.weekHeaderItem {
flex: 1;
text-align: center;
padding: 16rpx 0;
font-size: 28rpx;
font-weight: 600;
color: #333;
&.weekend {
color: @danger;
}
}
}
/* 日历网格 */
.calendarGrid {
display: flex;
flex-wrap: wrap;
width: 100%;
.calendarCell {
width: 14.28%;
height: 120rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1rpx solid #f0f0f0;
box-sizing: border-box;
position: relative;
&.empty {
background-color: #fafafa;
}
&.today {
background-color: rgba(39, 178, 95, 0.15);
border: 2rpx solid @primary;
}
&.weekend {
.dateNumber {
// color: @danger;
}
}
.dateNumber {
font-size: 24rpx;
font-weight: 500;
color: #333;
margin-bottom: 4rpx;
}
.scheduleInfo {
font-size: 24rpx;
padding: 2rpx 4rpx;
border-radius: 4rpx;
font-weight: 500;
text-align: center;
line-height: 1;
&.work {
// background-color: #333;
color: #000;
}
&.rest {
// background-color: #f5f5f5;
// color: #999;
color: red;
}
&.empty {
visibility: hidden;
}
}
}
}
}
/* 空状态样式 */
.emptyState,
.emptySchedule {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 40rpx;
text-align: center;
background: #fff;
border-radius: 16rpx;
margin-bottom: 32rpx;
box-shadow: @shadow;
.emptyIcon {
font-size: 80rpx;
margin-bottom: 24rpx;
opacity: 0.6;
}
.emptyText {
font-size: 32rpx;
color: #2c3e50;
font-weight: 600;
margin-bottom: 16rpx;
}
.emptyDesc {
font-size: 24rpx;
color: #666;
line-height: 1.5;
margin-bottom: 32rpx;
}
.retryBtn {
background: @primary;
color: #fff;
border: none;
border-radius: 24rpx;
padding: 16rpx 32rpx;
font-size: 24rpx;
font-weight: 600;
&::after {
border: none;
}
}
}
}
}
</style>