959 lines
35 KiB
Vue
959 lines
35 KiB
Vue
<!-- 考勤统计 - 重新设计版本 -->
|
||
<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.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="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">
|
||
<view class="cardIcon scheduleIcon">📋</view>
|
||
<view class="cardContent">
|
||
<text class="cardNumber">{{ getSummaryData().totalSchedule }}</text>
|
||
<text class="cardLabel">总排班</text>
|
||
</view>
|
||
</view>
|
||
<view class="summaryCard">
|
||
<view class="cardIcon attendIcon">✅</view>
|
||
<view class="cardContent">
|
||
<text class="cardNumber">{{ getSummaryData().totalAttend }}</text>
|
||
<text class="cardLabel">总出勤</text>
|
||
</view>
|
||
</view>
|
||
<view class="summaryCard">
|
||
<view class="cardIcon lateIcon">⏰</view>
|
||
<view class="cardContent">
|
||
<text class="cardNumber">{{ getSummaryData().totalLate }}</text>
|
||
<text class="cardLabel">迟到次数</text>
|
||
</view>
|
||
</view>
|
||
<view class="summaryCard">
|
||
<view class="cardIcon earlyIcon">🏃</view>
|
||
<view class="cardContent">
|
||
<text class="cardNumber">{{ getSummaryData().totalEarly }}</text>
|
||
<text class="cardLabel">早退次数</text>
|
||
</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">{{ getFirstChar(item.userName) }}</view>
|
||
<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>
|
||
|
||
<!-- 日历组件 -->
|
||
<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
|
||
}
|
||
},
|
||
onLoad() {
|
||
this.menu = uni.getMenuButtonBoundingClientRect()
|
||
let currentService = uni.getStorageSync('currentService')
|
||
this.serviceInfo = currentService
|
||
this.handleGetServerpartDetail(currentService.Serverpart_ID || currentService.SERVERPART_ID)
|
||
this.handleGetData(currentService.SERVERPART_NAME)
|
||
},
|
||
async onShow() {
|
||
let currentService = uni.getStorageSync('currentService')
|
||
if (currentService.Serverpart_ID !== this.serviceInfo.SERVERPART_ID && !this.isFirst) {
|
||
this.handleGetServerpartDetail(currentService.Serverpart_ID)
|
||
this.handleGetData(currentService.SERVERPART_NAME)
|
||
}
|
||
this.isFirst = false
|
||
},
|
||
methods: {
|
||
// 获取姓名首字符
|
||
getFirstChar(name) {
|
||
return name ? name.charAt(0).toUpperCase() : '?'
|
||
},
|
||
|
||
// 重试加载
|
||
retryLoad() {
|
||
if (this.retryCount < 3) {
|
||
this.retryCount++
|
||
this.handleGetData(this.serviceInfo.SERVERPART_NAME)
|
||
} 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)
|
||
}
|
||
},
|
||
|
||
// 显示日历
|
||
showCalendar() {
|
||
this.$refs.calendar.open()
|
||
},
|
||
// 日历确认
|
||
onCalendarConfirm(e) {
|
||
console.log('选择日期:', e)
|
||
this.selectDate = e.fulldate
|
||
this.handleGetData(this.serviceInfo.SERVERPART_NAME)
|
||
},
|
||
|
||
// 日历关闭
|
||
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.SERVERPART_NAME)
|
||
},
|
||
}
|
||
}
|
||
</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;
|
||
|
||
.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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style> |