ccy_DIB/pages/attendanceStatus/attendanceStatistics.vue
ylj20011123 ad1b7773d5 update
2025-08-27 09:06:31 +08:00

1310 lines
48 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>
<page-meta :page-style="'overflow:' + (showPopup ? 'hidden' : 'visible')"></page-meta>
<view class="main">
<!-- 自定义的页面顶部内容 -->
<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.SAName ? serviceInfo.SAName :
'请选择服务区' }}
</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="attendanceListBox" :style="{ paddingTop: (menu.bottom + 30) + 'px' }">
<!-- 头部标题和筛选 -->
<view class="headerSection">
<!-- 时间选择器 -->
<view class="dateSelector">
<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="filterBadge" v-if="attendanceStatisticsData.length > 0">
<text class="badgeText">{{ attendanceStatisticsData.length }}人</text>
</view>
</view>
</view>
<!-- 统计汇总卡片 -->
<view class="summaryCards" v-if="attendanceStatisticsData.length > 0">
<view class="summaryCard clickable" @click="showDetailModal('schedule')">
<view class="cardIcon scheduleIcon">📋</view>
<view class="cardContent">
<text class="cardNumber">{{ getSummaryData().totalSchedule }}</text>
<text class="cardLabel">总排班</text>
</view>
<view class="cardArrow"></view>
</view>
<view class="summaryCard clickable" @click="showDetailModal('attend')">
<view class="cardIcon attendIcon">✅</view>
<view class="cardContent">
<text class="cardNumber">{{ getSummaryData().totalAttend }}</text>
<text class="cardLabel">总出勤</text>
</view>
<view class="cardArrow"></view>
</view>
<view class="summaryCard clickable" @click="showDetailModal('late')">
<view class="cardIcon lateIcon">⏰</view>
<view class="cardContent">
<text class="cardNumber">{{ getSummaryData().totalLate }}</text>
<text class="cardLabel">迟到次数</text>
</view>
<view class="cardArrow"></view>
</view>
<view class="summaryCard clickable" @click="showDetailModal('early')">
<view class="cardIcon earlyIcon">🏃</view>
<view class="cardContent">
<text class="cardNumber">{{ getSummaryData().totalEarly }}</text>
<text class="cardLabel">早退次数</text>
</view>
<view class="cardArrow"></view>
</view>
</view>
<!-- 员工考勤列表 -->
<view class="employeeList">
<view class="employeeCard" v-for="(item, index) in attendanceStatisticsData" :key="index">
<!-- 员工基本信息 -->
<view class="employeeHeader">
<!-- 左侧:头像 + 带图标信息 -->
<view class="leftSection">
<view class="avatar" v-if="!item.phone">{{ getFirstChar(item.userName) }}
</view>
<image class="avatar" v-else
:src="`https://fwqznxj.yciccloud.com:9081/fileDownloadApi/bsys/file/thumbnail/download/${item.phone}`" />
<view class="iconInfoSection">
<view class="nameRow">
<image class="personIcon" src="https://eshangtech.com/cyy_DIB/personIcon.png">
</image>
<text class="employeeName">{{ item.userName || "-" }}</text>
</view>
<view class="phoneRow">
<image class="phoneIcon" src="https://eshangtech.com/cyy_DIB/phoneLabelIcon.png">
</image>
<text class="phoneNumber">{{ item.phone || item.userCode || "-" }}</text>
</view>
</view>
</view>
<!-- 右侧:无图标信息 -->
<view class="rightSection">
<text class="employeeJob">{{ item.userJob || "" }}</text>
<view class="workTypeBadge"
:class="(item.workType === '班' || item.workType === '差' || item.workType === '出' ? 'work' : 'rest')">
<text class="workTypeText">{{ item.workType || '正常' }}</text>
</view>
</view>
</view>
<!-- 考勤统计数据 -->
<view class="attendanceStats">
<view class="statItem">
<text class="statLabel">排班</text>
<text class="statValue">{{ item.scheduleTotal || 0 }}天</text>
</view>
<view class="statItem">
<text class="statLabel">出勤</text>
<text class="statValue success">{{ item.attendTotal || 0 }}天</text>
</view>
<view class="statItem">
<text class="statLabel">休息</text>
<text class="statValue">{{ item.restTotal || 0 }}天</text>
</view>
<view class="statItem">
<text class="statLabel">迟到</text>
<text class="statValue warning">{{ item.lateTotal || 0 }}次</text>
</view>
<view class="statItem">
<text class="statLabel">早退</text>
<text class="statValue danger">{{ item.earlyTotal || 0 }}次</text>
</view>
</view>
<!-- 当日打卡详情 -->
<view class="clockDetails" v-if="false">
<text class="sectionTitle">当日打卡记录</text>
<view class="clockRow">
<view class="clockItem">
<view class="clockIcon inIcon">🕐</view>
<view class="clockInfo">
<text class="clockLabel">上班打卡</text>
<text class="clockTime">{{ (item.dutyClockInTime && item.dutyClockInTime.trim()) ||
"未打卡" }}</text>
<text class="lateInfo" v-if="item.lateNum && item.lateNum > 0">迟到{{
formatDuration(item.lateNum) }}</text>
</view>
</view>
<view class="clockItem">
<view class="clockIcon outIcon">🕕</view>
<view class="clockInfo">
<text class="clockLabel">下班打卡</text>
<text class="clockTime">{{ (item.offDutyClockInTime &&
item.offDutyClockInTime.trim()) || "未打卡" }}</text>
<text class="earlyInfo" v-if="item.earlyNum && item.earlyNum > 0">早退{{
formatDuration(item.earlyNum) }}</text>
</view>
</view>
</view>
<view class="locationInfo" v-if="item.offDutyClockInPlace">
<image class="locationIcon" src="https://eshangtech.com/cyy_DIB/locationIcon.png" />
<text class="locationText">下班打卡地:{{ item.offDutyClockInPlace }}</text>
</view>
</view>
</view>
</view>
<!-- 空数据状态 - 只有在已加载完成且真的没有数据时才显示 -->
<view class="emptyState" v-if="attendanceStatisticsData.length === 0 && !loading && hasLoaded">
<view class="emptyIcon">📊</view>
<text class="emptyText">暂无考勤数据</text>
<text class="emptyDesc">请选择其他时间或服务区查看</text>
<!-- <button class="retryBtn" @tap="retryLoad" type="primary" size="mini">重新加载</button> -->
</view>
</view>
<!-- 详情弹窗 -->
<view class="detailModal" v-if="showDetailPopup" @click="closeDetailModal">
<view class="modalContent" @click.stop="">
<view class="modalHeader">
<text class="modalTitle">{{ getModalTitle() }}</text>
<view class="closeBtn" @click="closeDetailModal">×</view>
</view>
<view class="modalBody">
<view class="summaryInfo">
<text class="totalText">总计:{{ getCurrentModalTotal() }}</text>
<text class="countText">{{ attendanceStatisticsData.length }}人</text>
</view>
<view class="employeeRankList">
<view class="rankHeader">
<text class="rankTitle">人员排名</text>
</view>
<view class="rankScrollArea">
<view class="rankItem" v-for="(item, index) in getSortedData()" :key="index">
<view class="rankNumber"
:class="index === 0 ? 'rank-1' : index === 1 ? 'rank-2' : index === 2 ? 'rank-3' : ''">
{{ index + 1 }}</view>
<view class="employeeInfo">
<view class="employeeText">
<text class="name">{{ item.userName || "-" }}</text>
<text class="phone">{{ item.phone || item.userCode || "-" }}</text>
</view>
</view>
<view class="rankValue">
<text class="numberWithUnit">{{ getCurrentValue(item) }}{{ getCurrentUnit()
}}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 日历组件 -->
<uni-calendar ref="calendar" :insert="false" :mask-closable="true" @confirm="onCalendarConfirm"
@close="onCalendarClose" />
</view>
</template>
<script>
import request from "@/util/index.js";
import { formatTime } from '@/util/dateTime/index.js'
export default {
data() {
const nowDay = this.$util.cutDate(new Date(), 'YYYY-MM-DD')
return {
menu: {},
serviceInfo: {},
showPopup: false,
selectDate: nowDay,
attendanceStatisticsData: [],
isFirst: true,
seatInfo: {},
loading: false,
hasLoaded: false, // 是否已经完成过初始加载
retryCount: 0,
// 详情弹窗相关
showDetailPopup: false,
currentDetailType: 'schedule' // 'schedule', 'attend', 'late', 'early'
}
},
onLoad() {
this.menu = uni.getMenuButtonBoundingClientRect()
let currentService = uni.getStorageSync('currentService')
this.serviceInfo = currentService
// this.handleGetServerpartDetail(currentService.Serverpart_ID || currentService.SERVERPART_ID)
this.handleGetData(currentService.SAName)
},
async onShow() {
let currentService = uni.getStorageSync('currentService')
if (currentService.SACode !== this.serviceInfo.SACode && !this.isFirst) {
// this.handleGetServerpartDetail(currentService.Serverpart_ID)
this.serviceInfo = currentService
this.handleGetData(currentService.SAName)
}
this.isFirst = false
},
methods: {
// 获取姓名首字符
getFirstChar(name) {
return name ? name.charAt(0).toUpperCase() : '?'
},
// 重试加载
retryLoad() {
if (this.retryCount < 3) {
this.retryCount++
this.handleGetData(this.serviceInfo.SAName)
} else {
uni.showToast({
title: '多次重试失败,请检查网络连接',
icon: 'none'
})
}
},
// 格式化时长(分钟转小时分钟)
formatDuration(minutes) {
if (!minutes || minutes <= 0) return ''
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
if (hours > 0) {
return mins > 0 ? `${hours}小时${mins}分钟` : `${hours}小时`
}
return `${mins}分钟`
},
// 获取汇总数据
getSummaryData() {
const data = this.attendanceStatisticsData
return {
totalSchedule: data.reduce((sum, item) => sum + (item.scheduleTotal || 0), 0),
totalAttend: data.reduce((sum, item) => sum + (item.attendTotal || 0), 0),
totalLate: data.reduce((sum, item) => sum + (item.lateTotal || 0), 0),
totalEarly: data.reduce((sum, item) => sum + (item.earlyTotal || 0), 0)
}
},
// 显示详情弹窗
showDetailModal(type) {
this.currentDetailType = type
this.showDetailPopup = true
this.showPopup = true // 禁止页面滚动
},
// 关闭详情弹窗
closeDetailModal() {
this.showDetailPopup = false
this.showPopup = false // 恢复页面滚动
},
// 获取弹窗标题
getModalTitle() {
const titles = {
schedule: '排班情况详情',
attend: '出勤情况详情',
late: '迟到情况详情',
early: '早退情况详情'
}
return titles[this.currentDetailType] || '详情'
},
// 获取当前模态总数
getCurrentModalTotal() {
const summary = this.getSummaryData()
const totals = {
schedule: summary.totalSchedule + '天',
attend: summary.totalAttend + '天',
late: summary.totalLate + '次',
early: summary.totalEarly + '次'
}
return totals[this.currentDetailType] || '0'
},
// 获取单位
getCurrentUnit() {
const units = {
schedule: '天',
attend: '天',
late: '次',
early: '次'
}
return units[this.currentDetailType] || '天'
},
// 获取当前项的值
getCurrentValue(item) {
const values = {
schedule: item.scheduleTotal || 0,
attend: item.attendTotal || 0,
late: item.lateTotal || 0,
early: item.earlyTotal || 0
}
return values[this.currentDetailType] || 0
},
// 获取排序后的数据
getSortedData() {
const data = [...this.attendanceStatisticsData]
return data.sort((a, b) => {
const valueA = this.getCurrentValue(a)
const valueB = this.getCurrentValue(b)
return valueB - valueA // 从大到小排序
})
},
// 显示日历
showCalendar() {
this.$refs.calendar.open()
},
// 日历确认
onCalendarConfirm(e) {
console.log('选择日期:', e)
this.selectDate = e.fulldate
this.handleGetData(this.serviceInfo.SAName)
},
// 日历关闭
onCalendarClose() {
console.log('日历关闭')
},
// 拿到数据
async handleGetData(SERVERPART_NAME) {
let req = {
bsessionKey: "0B30475A94674D608022885F7763959B",
workTime: new Date(this.selectDate).getTime(),
saName: SERVERPART_NAME || "",
phone: "",
}
this.loading = true
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)
}
});
});
let list = data || []
if (list && list.length > 0) {
list.forEach((item) => {
item.dutyClockInTime = item.dutyClockInTime ? formatTime(item.dutyClockInTime) : ""
item.offDutyClockInTime = item.offDutyClockInTime ? formatTime(item.offDutyClockInTime) : ""
})
}
this.attendanceStatisticsData = list
this.hasLoaded = true // 标记已完成加载
} catch (error) {
console.error('获取考勤数据失败:', error)
uni.showToast({
title: '获取数据失败',
icon: 'none'
})
this.attendanceStatisticsData = []
} finally {
this.loading = false
uni.hideLoading()
}
},
// 查询服务区详情
async handleGetServerpartDetail(id) {
let currentService = uni.getStorageSync("currentService");
let seatInfo = uni.getStorageSync("seatInfo");
this.seatInfo = JSON.parse(seatInfo);
let req = {
ServerpartId: id || currentService.Serverpart_ID || currentService.SERVERPART_ID,
latitude: this.seatInfo.latitude,
longitude: this.seatInfo.longitude,
};
uni.showLoading({
title: "加载中...",
});
try {
const data = await request.$webJavaGet(
"/third-party/getServerPartInfo",
req
);
let obj = data.Result_Data;
this.serviceInfo = obj;
this.$forceUpdate();
} catch (error) {
console.error('获取服务区信息失败:', error)
} finally {
uni.hideLoading();
}
},
// 改变服务区
handleChangeService() {
this.$util.toNextRoute("navigateTo", "/pages/map/index?type=attendanceStatus");
},
handleBack() {
uni.navigateBack({
delta: 1
});
},
// 修改日期
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}`;
this.retryCount = 0 // 重置重试次数
await this.handleGetData(this.serviceInfo.SAName)
},
}
}
</script>
<style lang="less" scoped>
@bg: #f8f9fa;
@muted: #666;
@card: #fff;
@shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
@primary: #27B25F;
@primary2: #4CCC7F;
@success: #2ed573;
@warning: #ff9f43;
@danger: #ff4757;
@info: #3742fa;
.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);
.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: 14px;
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;
}
}
}
}
}
}
}
.attendanceListBox {
width: 100%;
box-sizing: border-box;
padding: 0 32rpx;
.headerSection {
margin-bottom: 32rpx;
.dateSelector {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx;
box-shadow: @shadow;
background: @card;
border-radius: 16rpx;
.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;
}
}
}
}
.filterBadge {
background: @primary;
border-radius: 20rpx;
padding: 0 8rpx;
.badgeText {
color: #fff;
font-size: 24rpx;
font-weight: 600;
}
}
}
}
.summaryCards {
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-bottom: 16rpx;
.summaryCard {
width: calc(50% - 8rpx);
box-sizing: border-box;
margin-bottom: 16rpx;
background: @card;
border-radius: 16rpx;
padding: 32rpx 24rpx;
box-shadow: @shadow;
display: flex;
align-items: center;
position: relative;
&.clickable {
cursor: pointer;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
background: #f8f9fa;
}
}
.cardIcon {
font-size: 40rpx;
margin-right: 20rpx;
width: 60rpx;
text-align: center;
&.scheduleIcon {
opacity: 0.8;
}
&.attendIcon {
opacity: 0.8;
}
&.lateIcon {
opacity: 0.8;
}
&.earlyIcon {
opacity: 0.8;
}
}
.cardContent {
flex: 1;
display: flex;
flex-direction: column;
.cardNumber {
font-size: 28rpx;
font-weight: 700;
color: #2c3e50;
line-height: 1.2;
}
.cardLabel {
font-size: 24rpx;
color: @muted;
}
}
.cardArrow {
font-size: 32rpx;
color: #ccc;
font-weight: bold;
margin-left: 8rpx;
}
}
}
.employeeList {
.employeeCard {
background: @card;
border-radius: 20rpx;
margin-bottom: 32rpx;
box-shadow: @shadow;
overflow: hidden;
.employeeHeader {
padding: 32rpx;
border-bottom: 2rpx solid #dcdddf;
display: flex;
align-items: flex-start;
justify-content: space-between;
/* 左侧区域:头像 + 带图标信息 */
.leftSection {
display: flex;
align-items: flex-start;
flex: 1;
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: linear-gradient(135deg, @primary, @primary2);
color: #fff;
font-size: 28rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
flex-shrink: 0;
}
.iconInfoSection {
display: flex;
flex-direction: column;
gap: 12rpx;
flex: 1;
padding-top: 8rpx;
.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: 20rpx;
height: 20rpx;
margin-right: 8rpx;
}
.phoneNumber {
font-size: 24rpx;
color: #666;
}
}
}
}
/* 右侧区域:无图标信息 */
.rightSection {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 12rpx;
padding-top: 8rpx;
.employeeJob {
font-size: 24rpx;
color: @muted;
text-align: right;
}
.workTypeBadge {
padding: 4rpx 10rpx;
border-radius: 12rpx;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 40rpx;
&.normal {
border-color: @info;
background: rgba(55, 66, 250, 0.1);
.workTypeText {
color: @info;
}
}
&.work {
border-color: @success;
background: #27B25F;
.workTypeText {
color: #fff;
}
}
&.rest {
background: rgba(154, 153, 152, 0.1);
.workTypeText {
color: #424141;
}
}
.workTypeText {
font-size: 20rpx;
font-weight: 500;
line-height: 1;
}
}
}
}
.attendanceStats {
padding: 24rpx;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
.statItem {
display: flex;
flex-direction: column;
align-items: center;
min-width: 120rpx;
.statLabel {
font-size: 24rpx;
color: @muted;
margin-bottom: 8rpx;
}
.statValue {
font-size: 28rpx;
font-weight: 700;
color: #2c3e50;
&.success {
color: @success;
}
&.warning {
color: @warning;
}
&.danger {
color: @danger;
}
}
}
}
.clockDetails {
padding: 32rpx;
border-top: 2rpx solid #f8f9fa;
background: #fbfcfd;
.sectionTitle {
font-size: 28rpx;
font-weight: 600;
color: #2c3e50;
margin-bottom: 24rpx;
}
.clockRow {
display: flex;
justify-content: space-between;
gap: 24rpx;
margin-bottom: 12rpx;
.clockItem {
flex: 1;
display: flex;
align-items: flex-start;
.clockIcon {
font-size: 32rpx;
margin-right: 16rpx;
margin-top: 4rpx;
&.inIcon {
opacity: 0.8;
}
&.outIcon {
opacity: 0.8;
}
}
.clockInfo {
flex: 1;
display: flex;
flex-direction: column;
.clockLabel {
font-size: 24rpx;
color: @muted;
margin-bottom: 4rpx;
}
.clockTime {
font-size: 24rpx;
font-weight: 600;
color: #2c3e50;
margin-bottom: 4rpx;
white-space: nowrap;
}
.lateInfo {
font-size: 20rpx;
color: @warning;
}
.earlyInfo {
font-size: 20rpx;
color: @danger;
}
}
}
}
.locationInfo {
display: flex;
align-items: center;
padding: 16rpx 20rpx;
background: #fff;
border-radius: 12rpx;
.locationIcon {
width: 24rpx;
height: 24rpx;
margin-right: 12rpx;
}
.locationText {
font-size: 24rpx;
color: @muted;
}
}
}
}
}
.emptyState {
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;
}
}
}
}
// 详情弹窗样式
.detailModal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 60rpx 40rpx;
.modalContent {
width: calc(100vw - 64rpx);
max-height: 80vh;
background: #fff;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
.modalHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 40rpx;
border-bottom: 2rpx solid #f5f5f5;
background: linear-gradient(135deg, @primary, @primary2);
.modalTitle {
font-size: 32rpx;
font-weight: 700;
color: #fff;
}
.closeBtn {
width: 48rpx;
height: 48rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #fff;
font-weight: bold;
cursor: pointer;
&:active {
background: rgba(255, 255, 255, 0.3);
}
}
}
.modalBody {
padding: 32rpx 40rpx;
max-height: 60vh;
.summaryInfo {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx;
background: #f8f9fa;
border-radius: 16rpx;
margin-bottom: 32rpx;
.totalText {
font-size: 28rpx;
font-weight: 600;
color: #2c3e50;
}
.countText {
font-size: 24rpx;
color: @muted;
background: #fff;
padding: 8rpx 16rpx;
border-radius: 20rpx;
}
}
.employeeRankList {
.rankHeader {
margin-bottom: 24rpx;
.rankTitle {
font-size: 28rpx;
font-weight: 600;
color: #2c3e50;
}
}
.rankScrollArea {
max-height: 40vh;
overflow-y: auto;
/* 隐藏滚动条 */
&::-webkit-scrollbar {
display: none;
}
/* 兼容其他浏览器 */
-ms-overflow-style: none;
scrollbar-width: none;
}
.rankItem {
display: flex;
align-items: center;
padding: 24rpx 20rpx;
background: #fff;
border-radius: 12rpx;
margin-bottom: 16rpx;
border: 2rpx solid #f5f5f5;
&:nth-child(1) {
// 第一名
border-color: #ffd700;
background: linear-gradient(135deg, #fff9e6, #fff);
}
&:nth-child(2) {
// 第二名
border-color: #c0c0c0;
background: linear-gradient(135deg, #f8f8f8, #fff);
}
&:nth-child(3) {
// 第三名
border-color: #cd7f32;
background: linear-gradient(135deg, #fdf2e9, #fff);
}
.rankNumber {
width: 48rpx;
height: 48rpx;
background: @primary;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
font-weight: 700;
margin-right: 20rpx;
flex-shrink: 0;
&.rank-1 {
background: linear-gradient(135deg, #ffd700, #ffed4e);
color: #333;
box-shadow: 0 4rpx 12rpx rgba(255, 215, 0, 0.3);
}
&.rank-2 {
background: linear-gradient(135deg, #c0c0c0, #e8e8e8);
color: #333;
box-shadow: 0 4rpx 12rpx rgba(192, 192, 192, 0.3);
}
&.rank-3 {
background: linear-gradient(135deg, #cd7f32, #daa560);
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(205, 127, 50, 0.3);
}
}
.employeeInfo {
flex: 1;
display: flex;
align-items: center;
.employeeText {
flex: 1;
display: flex;
flex-direction: column;
.name {
font-size: 28rpx;
font-weight: 600;
color: #2c3e50;
line-height: 1.3;
margin-bottom: 4rpx;
}
.phone {
font-size: 24rpx;
color: @muted;
}
}
}
.rankValue {
display: flex;
align-items: center;
justify-content: flex-end;
.numberWithUnit {
font-size: 32rpx;
font-weight: 700;
color: @primary;
line-height: 1.2;
}
}
}
}
}
}
}
}
</style>