1259 lines
32 KiB
Vue
1259 lines
32 KiB
Vue
<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>
|