2025-10-27 23:13:08 +08:00

1259 lines
32 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="attendance-container">
<!-- 移动设备顶部状态栏 -->
<view class="top_bar flex w-full" v-if="isMobile">
<view class="chat-header">
<view class="header-left" @click="goBack">
<i class="fas fa-arrow-left"></i>
</view>
<view class="header-title">考勤打卡</view>
<view class="header-right" @click="showMoreOptions">
<i class="fas fa-ellipsis-v"></i>
</view>
</view>
</view>
<!-- 浏览器环境下的导航栏 -->
<view class="chat-header" v-else>
<view class="header-left" @click="goBack">
<i class="fas fa-arrow-left"></i>
</view>
<view class="header-title">考勤打卡</view>
<view class="header-right" @click="showMoreOptions">
<i class="fas fa-ellipsis-v"></i>
</view>
</view>
<!-- 顶部日期与问候语 -->
<view class="header">
<!-- <view class="date">
{{ currentDate }}
</view>
<view class="greeting">
{{ greeting }}, {{ userName }}
</view> -->
</view>
<!-- Tab切换 -->
<view class="tab-container">
<view
class="tab-item"
:class="{ active: activeTab === 'work' }"
@tap="switchTab('work')"
>
上下班打卡
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'out' }"
@tap="switchTab('out')"
>
外出打卡
</view>
</view>
<!-- 考勤摘要模块 -->
<view class="attendance-summary flex-column">
<view class="flex-between underline pb-30">
<view class="status">
<view class="label">上班时间</view>
<view class="value">{{ workStart }}</view>
</view>
<view class="status">
<view class="label">下班时间</view>
<view class="value">{{ workEnd }}</view>
</view>
<view class="status">
<view class="label">已打卡天数</view>
<view class="value">{{ punchDays }}</view>
</view>
</view>
<view class="flex mt-30 tools-bar" >
<view @tap="goToRecord">记录</view>
<view @tap="goToStatistics">统计</view>
<view @tap="goToRule">规则</view>
</view>
</view>
<!-- 上下班打卡区域 -->
<view class="clockin-area" v-if="activeTab === 'work'">
<view class="circle" @tap="handleWorkPunch">
<view class="punch-time">{{ currentTime }}</view>
<view class="punch-status" v-if="workRecorded">
<text class="status-success">已打卡</text>
</view>
<view v-else class="punch-btn">
{{ workPunchText }}
</view>
</view>
<view class="location">
<text class="iconfont icon-dizhi"></text>
{{ location }}
<button class="location-refresh" size="mini" @tap="refreshLocation">
刷新定位
</button>
</view>
</view>
<!-- 外出打卡区域 -->
<view class="out-area" v-if="activeTab === 'out'">
<view class="out-form">
<view class="form-item">
<text class="label">开始时间</text>
<picker
mode="time"
:value="startTime"
@change="onStartTimeChange"
class="time-picker"
>
<view class="picker-text">{{ startTime || "请选择开始时间" }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">结束时间</text>
<picker
mode="time"
:value="endTime"
@change="onEndTimeChange"
class="time-picker"
>
<view class="picker-text">{{ endTime || "请选择结束时间" }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">外出时长</text>
<view class="duration-display">
<text class="duration-text">{{ calculatedDuration }}</text>
</view>
</view>
<view class="form-item">
<text class="label">外出事由</text>
<textarea
class="textarea"
v-model="outReason"
placeholder="请输入外出事由8000字以内"
placeholder-style="color: #999"
:maxlength="8000"
:show-count="true"
auto-height
/>
</view>
</view>
<view class="out-punch-btn">
<button
class="punch-btn"
:disabled="!canOutPunch"
@tap="handleOutPunch"
>
提交外出申请
</button>
</view>
</view>
<!-- 打卡记录弹窗 -->
<view class="record-modal" v-if="showRecordModal" @tap="closeRecordModal">
<view class="modal-content" @tap.stop>
<view class="modal-header">
<view class="modal-title">打卡记录</view>
<view class="close-btn" @tap="closeRecordModal">
<i class="fas fa-times"></i>
</view>
</view>
<view class="modal-body">
<view v-if="todayRecords.length > 0">
<view
class="record-item"
v-for="(record, idx) in todayRecords"
:key="idx"
>
<view class="record-left">
<view class="record-time">{{ formatRecordTime(record.time) }}</view>
<view class="record-type">{{ record.type }}</view>
<view v-if="record.reason" class="record-detail"
>事由{{ record.reason }}</view
>
<view v-if="record.startTime" class="record-detail"
>开始时间{{ record.startTime }}</view
>
<view v-if="record.endTime" class="record-detail"
>结束时间{{ record.endTime }}</view
>
<view v-if="record.duration" class="record-detail"
>外出时长{{ record.duration }}</view
>
</view>
</view>
</view>
<view v-else class="no-record">暂无打卡记录</view>
</view>
</view>
</view>
<!-- 考勤规则弹窗 -->
<view class="rule-modal" v-if="showRuleModal" @tap="closeRuleModal">
<view class="modal-content" @tap.stop>
<view class="modal-header">
<view class="modal-title">考勤规则</view>
<view class="close-btn" @tap="closeRuleModal">
<i class="fas fa-times"></i>
</view>
</view>
<view class="modal-body">
<view class="rule-sections">
<!-- 考勤时间 -->
<view class="rule-section">
<view class="rule-header" @tap="toggleRuleSection('attendance')">
<view class="rule-title">
<i class="fas fa-clock"></i>
考勤时间
</view>
<view class="rule-arrow" :class="{ expanded: expandedRules.attendance }">
<i class="fas fa-chevron-down"></i>
</view>
</view>
<view class="rule-content" v-if="expandedRules.attendance">
<view class="rule-item">
<text class="rule-label">上班时间</text>
<text class="rule-value">09:00</text>
</view>
<view class="rule-item">
<text class="rule-label">下班时间</text>
<text class="rule-value">18:00</text>
</view>
<view class="rule-item">
<text class="rule-label">午休时间</text>
<text class="rule-value">12:00 - 13:00</text>
</view>
<view class="rule-item">
<text class="rule-label">弹性时间</text>
<text class="rule-value">±30分钟</text>
</view>
</view>
</view>
<!-- 补卡规则 -->
<view class="rule-section">
<view class="rule-header" @tap="toggleRuleSection('supplement')">
<view class="rule-title">
<i class="fas fa-edit"></i>
补卡规则
</view>
<view class="rule-arrow" :class="{ expanded: expandedRules.supplement }">
<i class="fas fa-chevron-down"></i>
</view>
</view>
<view class="rule-content" v-if="expandedRules.supplement">
<view class="rule-item">
<text class="rule-label">补卡时限</text>
<text class="rule-value">当日24:00</text>
</view>
<view class="rule-item">
<text class="rule-label">补卡次数</text>
<text class="rule-value">每月最多3次</text>
</view>
<view class="rule-item">
<text class="rule-label">补卡原因</text>
<text class="rule-value">必须填写详细说明</text>
</view>
<view class="rule-item">
<text class="rule-label">审批流程</text>
<text class="rule-value">直属领导审批</text>
</view>
</view>
</view>
<!-- 更多规则 -->
<view class="rule-section">
<view class="rule-header" @tap="toggleRuleSection('more')">
<view class="rule-title">
<i class="fas fa-cog"></i>
更多规则
</view>
<view class="rule-arrow" :class="{ expanded: expandedRules.more }">
<i class="fas fa-chevron-down"></i>
</view>
</view>
<view class="rule-content" v-if="expandedRules.more">
<view class="rule-item">
<text class="rule-label">迟到处理</text>
<text class="rule-value">超过30分钟算迟到</text>
</view>
<view class="rule-item">
<text class="rule-label">早退处理</text>
<text class="rule-value">提前30分钟以上算早退</text>
</view>
<view class="rule-item">
<text class="rule-label">外出申请</text>
<text class="rule-value">需提前1小时申请</text>
</view>
<view class="rule-item">
<text class="rule-label">请假制度</text>
<text class="rule-value">按公司请假制度执行</text>
</view>
<view class="rule-item">
<text class="rule-label">加班规定</text>
<text class="rule-value">超过18:30算加班</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
userName: "张三",
currentDate: "",
greeting: "",
currentTime: "",
workStart: "09:00",
workEnd: "18:00",
punchDays: 12,
workRecorded: false,
location: "正在定位中...",
todayRecords: [],
activeTab: "work", // 当前激活的tab
// 外出打卡相关数据
outReason: "",
startTime: "",
endTime: "",
outRecorded: false,
// 弹窗相关数据
showRecordModal: false,
showRuleModal: false,
expandedRules: {
attendance: true, // 默认展开第一个
supplement: false,
more: false
},
};
},
onLoad() {
this.updateDateTime();
this.greeting = this.getGreeting();
this.refreshLocation();
this.loadTodayRecords();
// 更新时间
this.timer = setInterval(this.updateDateTime, 1000);
},
onUnload() {
clearInterval(this.timer);
},
computed: {
// 从全局数据获取设备信息
isMobile() {
return getApp().globalData.isMobile;
},
workPunchText() {
// 根据今日打卡记录判断是上班还是下班
const workRecords = this.todayRecords.filter(
(record) => record.type === "上班打卡" || record.type === "下班打卡"
);
if (workRecords.length === 0) {
return "上班打卡";
} else if (workRecords.length === 1) {
return "下班打卡";
} else {
return "今日已打卡";
}
},
canOutPunch() {
return (
this.startTime &&
this.endTime &&
this.outReason.trim() &&
this.calculatedDuration !== "时间无效"
);
},
calculatedDuration() {
if (!this.startTime || !this.endTime) {
return "请选择开始和结束时间";
}
const start = this.parseTime(this.startTime);
const end = this.parseTime(this.endTime);
if (!start || !end) {
return "时间格式错误";
}
if (end <= start) {
return "结束时间必须晚于开始时间";
}
const diffMs = end - start;
const hours = Math.floor(diffMs / (1000 * 60 * 60));
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
if (hours > 0) {
return `${hours}小时${minutes}分钟`;
} else {
return `${minutes}分钟`;
}
},
},
methods: {
updateDateTime() {
const now = new Date();
this.currentDate = `${now.getFullYear()}-${(now.getMonth() + 1)
.toString()
.padStart(2, "0")}-${now.getDate().toString().padStart(2, "0")}`;
this.currentTime = now.toTimeString().slice(0, 8);
},
getGreeting() {
const hour = new Date().getHours();
if (hour < 6) return "夜深了";
if (hour < 9) return "早上好";
if (hour < 12) return "上午好";
if (hour < 14) return "中午好";
if (hour < 18) return "下午好";
if (hour < 22) return "晚上好";
return "夜深了";
},
switchTab(tab) {
this.activeTab = tab;
},
handleWorkPunch() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const time = `${year}-${month}-${day} ${this.currentTime}`;
// 这里只做前端模拟,实际要调用接口
let type = "";
const workRecords = this.todayRecords.filter(
(record) => record.type === "上班打卡" || record.type === "下班打卡"
);
if (workRecords.length === 0) {
type = "上班打卡";
} else if (workRecords.length === 1) {
type = "下班打卡";
} else {
uni.showToast({ title: "今日已打卡完成", icon: "none" });
return;
}
this.todayRecords.push({ time, type });
this.workRecorded = true;
uni.showToast({ title: `${type}成功`, icon: "success" });
setTimeout(() => {
this.workRecorded = false;
}, 2000);
},
handleOutPunch() {
if (!this.canOutPunch) {
if (!this.startTime || !this.endTime) {
uni.showToast({ title: "请选择开始和结束时间", icon: "none" });
} else if (this.calculatedDuration === "结束时间必须晚于开始时间") {
uni.showToast({ title: "结束时间必须晚于开始时间", icon: "none" });
} else if (!this.outReason.trim()) {
uni.showToast({ title: "请填写外出事由", icon: "none" });
} else {
uni.showToast({ title: "请填写完整信息", icon: "none" });
}
return;
}
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const time = `${year}-${month}-${day} ${this.currentTime}`;
const type = "外出申请";
const record = {
time,
type,
reason: this.outReason,
startTime: this.startTime,
endTime: this.endTime,
duration: this.calculatedDuration,
};
this.todayRecords.push(record);
this.outRecorded = true;
uni.showToast({ title: "外出申请提交成功", icon: "success" });
// 清空表单
this.outReason = "";
this.startTime = "";
this.endTime = "";
setTimeout(() => {
this.outRecorded = false;
}, 2000);
},
onStartTimeChange(e) {
this.startTime = e.detail.value;
},
onEndTimeChange(e) {
this.endTime = e.detail.value;
},
parseTime(timeStr) {
if (!timeStr) return null;
const [hours, minutes] = timeStr.split(':').map(Number);
if (isNaN(hours) || isNaN(minutes)) return null;
const today = new Date();
today.setHours(hours, minutes, 0, 0);
return today;
},
refreshLocation() {
// 模拟定位
this.location = "北京·中关村";
// 若已集成定位api可替换
// uni.getLocation({...});
},
loadTodayRecords() {
// 模拟从本地加载今日打卡
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
this.todayRecords = [
{
time: `${year}-${month}-${day} 09:15:30`,
type: "上班打卡"
},
{
time: `${year}-${month}-${day} 12:30:45`,
type: "外出申请",
reason: "客户拜访",
startTime: "12:30",
endTime: "14:00",
duration: "1小时30分钟"
},
{
time: `${year}-${month}-${day} 18:20:15`,
type: "下班打卡"
}
];
},
/**
* 返回
*/
goBack() {
uni.navigateBack();
},
/**
* 显示更多选项
*/
showMoreOptions() {
uni.showActionSheet({
itemList: ["考勤记录", "设置", "帮助"],
success: (res) => {
switch (res.tapIndex) {
case 0:
// 考勤记录
this.showRecordModal = true;
break;
case 1:
// 设置
uni.showToast({ title: "设置功能开发中", icon: "none" });
break;
case 2:
// 帮助
uni.showToast({ title: "帮助功能开发中", icon: "none" });
break;
}
},
});
},
/**
* 显示打卡记录
*/
goToRecord() {
this.showRecordModal = true;
},
/**
* 关闭打卡记录弹窗
*/
closeRecordModal() {
this.showRecordModal = false;
},
/**
* 跳转到统计页面
*/
goToStatistics() {
uni.navigateTo({
url: "/pages/hr/attendance/statistics",
});
},
/**
* 显示考勤规则弹窗
*/
goToRule() {
this.showRuleModal = true;
},
/**
* 关闭考勤规则弹窗
*/
closeRuleModal() {
this.showRuleModal = false;
},
/**
* 切换规则栏目展开状态
*/
toggleRuleSection(section) {
this.expandedRules[section] = !this.expandedRules[section];
},
/**
* 格式化记录时间显示
*/
formatRecordTime(timeStr) {
if (!timeStr) return '';
// 如果是完整的时间字符串(包含年月日)
if (timeStr.includes('-') || timeStr.includes('/')) {
const date = new Date(timeStr);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
// 如果只是时间字符串,添加今天的日期
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `${year}-${month}-${day} ${timeStr}`;
},
},
};
</script>
<style lang="scss" scoped>
.attendance-container {
min-height: 100vh;
background: #f4f6fa;
padding: 0;
}
/* 移动设备顶部状态栏 */
.top_bar {
background: var(--gradient-primary);
box-shadow: var(--shadow-lg);
z-index: 9999;
position: fixed;
top: 0;
left: 0;
right: 0;
height: calc(var(--status-bar-height) + 88rpx);
display: flex;
align-items: flex-end;
padding-top: var(--status-bar-height);
box-sizing: border-box;
}
/* 支持安全区域的设备 */
@supports (padding: max(0px)) {
.top_bar {
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
}
.top_bar + .header {
margin-top: calc(
var(--status-bar-height) + 88rpx + env(safe-area-inset-top)
);
}
}
/* 顶部导航栏 */
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 88rpx;
background-color: var(--surface);
border-bottom: 1rpx solid var(--border);
padding: 0 20rpx;
box-sizing: border-box;
box-shadow: var(--shadow);
}
/* 浏览器环境下的固定定位 */
.attendance-container .chat-header:not(.top_bar .chat-header) {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 9999;
height: calc(var(--status-bar-height) + 88rpx);
padding-top: var(--status-bar-height);
}
/* 移动设备下的导航栏样式 */
.top_bar .chat-header {
background: transparent;
border-bottom: none;
box-shadow: none;
width: 100%;
height: 88rpx;
padding: 0 20rpx;
box-sizing: border-box;
}
.top_bar .header-title {
color: var(--white);
font-weight: 600;
font-size: 36rpx;
}
.top_bar .header-left i,
.top_bar .header-right i {
color: var(--white);
font-size: 40rpx;
}
.header-left,
.header-right {
width: 80rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease;
}
.header-right:active {
background-color: var(--surface-hover);
border-radius: 8rpx;
}
/* 移动设备下的按钮悬停效果 */
.top_bar .header-left:active,
.top_bar .header-right:active {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 8rpx;
}
.header-title {
font-size: 32rpx;
font-weight: 600;
color: var(--title-color);
}
/* 浏览器环境下为内容添加顶部间距 */
.attendance-container .chat-header:not(.top_bar .chat-header) + .header {
margin-top: calc(var(--status-bar-height) + 88rpx);
}
@supports (padding: max(0px)) {
.attendance-container .chat-header:not(.top_bar .chat-header) {
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
}
.attendance-container .chat-header:not(.top_bar .chat-header) + .header {
margin-top: calc(
var(--status-bar-height) + 88rpx + env(safe-area-inset-top)
);
}
}
.header {
padding: 48rpx 36rpx 12rpx 36rpx;
// background: linear-gradient(90deg, #5497ff 0%, #88bafe 100%);
color: #fff;
.date {
font-size: 28rpx;
margin-bottom: 8rpx;
opacity: 0.9;
}
.greeting {
font-size: 40rpx;
font-weight: bold;
}
}
.tab-container {
background: #fff;
margin: -32rpx 36rpx 0 36rpx;
border-radius: 16rpx 16rpx 0 0;
border-bottom: 1rpx solid #efefefef;
display: flex;
box-shadow: 0 -6rpx 18rpx 0 rgba(74, 144, 226, 0.04);
position: relative;
z-index: 1;
.tab-item {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 28rpx;
color: #9eaab7;
position: relative;
transition: all 0.3s ease;
&.active {
color: #388bff;
font-weight: bold;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background: #388bff;
border-radius: 2rpx;
}
}
}
}
.attendance-summary {
background: #fff;
box-shadow: 0 6rpx 18rpx 0 rgba(74, 144, 226, 0.04);
border-radius: 16rpx;
padding: 36rpx 30rpx;
margin: -32rpx 36rpx 32rpx 36rpx;
display: flex;
justify-content: space-between;
.status {
text-align: center;
margin-top: 30rpx;
.label {
color: #9eaab7;
font-size: 24rpx;
}
.value {
color: #2d4259;
font-size: 32rpx;
margin-top: 8rpx;
font-weight: bold;
}
}
.tools-bar {
font-size: 28rpx;
color: #388bff;
font-weight: bold;
display: flex;
justify-content: space-around;
align-items: center;
}
}
.clockin-area {
display: flex;
flex-direction: column;
align-items: center;
margin: 240rpx 0 0 0;
.circle {
width: 320rpx;
height: 320rpx;
border-radius: 50%;
background: #fff;
box-shadow: 0 8rpx 18rpx 0 rgba(74, 144, 226, 0.06);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
.punch-time {
font-size: 60rpx;
font-weight: bold;
color: #2d4259;
margin-bottom: 8rpx;
}
.punch-status {
margin-top: 16rpx;
.status-success {
color: #66c27c;
font-size: 32rpx;
}
}
.punch-btn {
border: none;
color: #388bff;
font-size: 32rpx;
}
}
.location {
margin-top: 26rpx;
color: #90a4b7;
font-size: 24rpx;
display: flex;
align-items: center;
.iconfont {
margin-right: 10rpx;
font-size: 26rpx;
}
.location-refresh {
background: none;
color: #388bff;
margin-left: 18rpx;
font-size: 22rpx;
border: none;
padding: 0 10rpx;
}
}
}
.out-area {
margin: 24rpx 36rpx 0 36rpx;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 8rpx 18rpx 0 rgba(74, 144, 226, 0.06);
padding: 36rpx 30rpx;
.out-form {
.form-item {
margin-bottom: 32rpx;
.label {
display: block;
font-size: 28rpx;
color: #2d4259;
margin-bottom: 16rpx;
font-weight: 500;
}
.input {
width: 100%;
height: 80rpx;
background: #f8f9fa;
border: 1rpx solid #e9ecef;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #2d4259;
box-sizing: border-box;
&:focus {
border-color: #388bff;
background: #fff;
}
}
.textarea {
width: 100%;
min-height: 120rpx;
background: #f8f9fa;
border: 1rpx solid #e9ecef;
border-radius: 12rpx;
padding: 20rpx 24rpx;
font-size: 28rpx;
color: #2d4259;
box-sizing: border-box;
line-height: 1.5;
&:focus {
border-color: #388bff;
background: #fff;
}
}
.duration-display {
width: 100%;
height: 80rpx;
background: #f8f9fa;
border: 1rpx solid #e9ecef;
border-radius: 12rpx;
display: flex;
align-items: center;
padding: 0 24rpx;
box-sizing: border-box;
.duration-text {
font-size: 28rpx;
color: #2d4259;
font-weight: 500;
}
}
.time-picker {
width: 100%;
height: 80rpx;
background: #f8f9fa;
border: 1rpx solid #e9ecef;
border-radius: 12rpx;
display: flex;
align-items: center;
padding: 0 24rpx;
box-sizing: border-box;
.picker-text {
font-size: 28rpx;
color: #2d4259;
}
&:active {
border-color: #388bff;
background: #fff;
}
}
}
}
.out-punch-btn {
margin-top: 40rpx;
text-align: center;
.punch-btn {
background: linear-gradient(90deg, #388bff 0%, #62b3ff 100%);
border: none;
color: #fff;
font-size: 32rpx;
border-radius: 64rpx;
width: 200rpx;
height: 80rpx;
box-shadow: 0 4rpx 10rpx 0 rgba(56, 139, 255, 0.17);
&:disabled {
background: #e9ecef;
color: #9eaab7;
box-shadow: none;
}
}
}
}
.record-section {
margin: 48rpx 36rpx 0 36rpx;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 4rpx 10rpx 0 rgba(74, 144, 226, 0.04);
padding: 30rpx 24rpx;
.record-title {
font-size: 30rpx;
font-weight: bold;
margin-bottom: 18rpx;
color: #2d4259;
}
.record-item {
padding: 24rpx 0;
border-bottom: 1px solid #f2f3f8;
&:last-child {
border-bottom: none;
}
.record-left {
.record-time {
font-size: 28rpx;
color: #2d4259;
font-weight: 500;
margin-bottom: 8rpx;
}
.record-type {
color: #388bff;
font-size: 26rpx;
font-weight: 500;
margin-bottom: 8rpx;
}
.record-detail {
font-size: 24rpx;
color: #9eaab7;
margin-bottom: 4rpx;
line-height: 1.4;
}
}
}
.no-record {
color: #aaa;
text-align: center;
padding: 26rpx 0;
font-size: 28rpx;
}
}
/* 打卡记录弹窗 */
.record-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.modal-content {
background: #fff;
border-radius: 16rpx;
width: 100%;
max-width: 600rpx;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 20rpx 40rpx rgba(0, 0, 0, 0.15);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 40rpx;
border-bottom: 1rpx solid #f0f0f0;
background: #fff;
position: sticky;
top: 0;
z-index: 1;
}
.modal-title {
font-size: 32rpx;
font-weight: 600;
color: #2d4259;
}
.close-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #f5f5f5;
color: #666;
font-size: 28rpx;
transition: all 0.2s ease;
}
.close-btn:active {
background: #e0e0e0;
transform: scale(0.95);
}
.modal-body {
padding: 0 40rpx 40rpx 40rpx;
max-height: 60vh;
overflow-y: auto;
}
.modal-body .record-item {
padding: 24rpx 0;
border-bottom: 1rpx solid #f2f3f8;
&:last-child {
border-bottom: none;
}
.record-left {
.record-time {
font-size: 28rpx;
color: #2d4259;
font-weight: 500;
margin-bottom: 8rpx;
}
.record-type {
color: #388bff;
font-size: 26rpx;
font-weight: 500;
margin-bottom: 8rpx;
}
.record-detail {
font-size: 24rpx;
color: #9eaab7;
margin-bottom: 4rpx;
line-height: 1.4;
}
}
}
.modal-body .no-record {
color: #aaa;
text-align: center;
padding: 60rpx 0;
font-size: 28rpx;
}
/* 考勤规则弹窗 */
.rule-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.rule-sections {
.rule-section {
margin-bottom: 20rpx;
border-radius: 12rpx;
overflow: hidden;
background: #f8f9fa;
border: 1rpx solid #e9ecef;
.rule-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 30rpx;
background: #fff;
border-bottom: 1rpx solid #e9ecef;
cursor: pointer;
transition: all 0.2s ease;
&:active {
background: #f5f5f5;
}
.rule-title {
display: flex;
align-items: center;
font-size: 30rpx;
font-weight: 600;
color: #2d4259;
i {
margin-right: 16rpx;
color: #388bff;
font-size: 28rpx;
}
}
.rule-arrow {
color: #9eaab7;
font-size: 24rpx;
transition: transform 0.3s ease;
&.expanded {
transform: rotate(180deg);
}
}
}
.rule-content {
padding: 0 30rpx 24rpx 30rpx;
background: #fff;
.rule-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.rule-label {
font-size: 26rpx;
color: #666;
flex: 1;
}
.rule-value {
font-size: 26rpx;
color: #2d4259;
font-weight: 500;
text-align: right;
}
}
}
}
}
</style>