|
|
@@ -0,0 +1,877 @@ |
|
|
|
<template> |
|
|
|
<div class="page"> |
|
|
|
<div class="container"> |
|
|
|
<!-- 用户信息头部 --> |
|
|
|
<div class="header"> |
|
|
|
<div class="user-info"> |
|
|
|
<div class="user-left"> |
|
|
|
<div class="avatar"> |
|
|
|
<el-avatar :src="userinfo.avatar">{{ |
|
|
|
userinfo.nickName |
|
|
|
}}</el-avatar> |
|
|
|
</div> |
|
|
|
<div class="user-name">{{ userinfo.nickName }}</div> |
|
|
|
</div> |
|
|
|
<div class="user-right"> |
|
|
|
<div class="user-department"> |
|
|
|
{{ userinfo.dept.deptName }} |
|
|
|
<em v-if="userinfo.language">({{ userinfo.language }})</em> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 月份筛选 --> |
|
|
|
<div class="filter-section"> |
|
|
|
<div class="month-tabs"> |
|
|
|
<div |
|
|
|
v-for="month in monthOptions" |
|
|
|
:key="month.value" |
|
|
|
class="month-tab" |
|
|
|
:class="{ active: selectedMonth === month.value }" |
|
|
|
@click="handleMonthChange(month.value)" |
|
|
|
> |
|
|
|
{{ $t(month.labelKey) }} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 统计信息 --> |
|
|
|
<div class="statistics-section"> |
|
|
|
<div class="statistics-cards"> |
|
|
|
<div class="statistics-card"> |
|
|
|
<div class="statistics-number">{{ statisticsData.totalDays }}</div> |
|
|
|
<div class="statistics-label"> |
|
|
|
{{ $t('checkin.record.statistics.totalDays') }} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="statistics-card"> |
|
|
|
<div class="statistics-number late"> |
|
|
|
{{ statisticsData.lateDays }} |
|
|
|
</div> |
|
|
|
<div class="statistics-label"> |
|
|
|
{{ $t('checkin.record.statistics.lateDays') }} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="statistics-card"> |
|
|
|
<div class="statistics-number early"> |
|
|
|
{{ statisticsData.earlyDays }} |
|
|
|
</div> |
|
|
|
<div class="statistics-label"> |
|
|
|
{{ $t('checkin.record.statistics.earlyDays') }} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="statistics-card"> |
|
|
|
<div class="statistics-number normal"> |
|
|
|
{{ statisticsData.normalDays }} |
|
|
|
</div> |
|
|
|
<div class="statistics-label"> |
|
|
|
{{ $t('checkin.record.statistics.normalDays') }} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 考勤记录列表 --> |
|
|
|
<div class="record-section"> |
|
|
|
<div v-if="loading" class="loading-wrapper"> |
|
|
|
<i class="el-icon-loading" /> |
|
|
|
<span>{{ $t('checkin.record.loading') }}</span> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div v-else-if="attendanceList.length === 0" class="empty-wrapper"> |
|
|
|
<div class="empty-text">{{ $t('checkin.record.empty') }}</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div v-else class="record-list"> |
|
|
|
<div |
|
|
|
v-for="record in attendanceList" |
|
|
|
:key="record.id" |
|
|
|
class="record-item" |
|
|
|
:class="{ |
|
|
|
'is-today': isToday(record.checkInTime), |
|
|
|
'is-weekend': isWeekend(record.checkInTime) |
|
|
|
}" |
|
|
|
> |
|
|
|
<div class="record-date"> |
|
|
|
<div class="date-day"> |
|
|
|
{{ formatDateDay(record.checkInTime) }} |
|
|
|
</div> |
|
|
|
<div class="date-month"> |
|
|
|
{{ formatDateMonth(record.checkInTime) }} |
|
|
|
</div> |
|
|
|
<div |
|
|
|
class="date-weekday" |
|
|
|
:class="{ |
|
|
|
'is-today': isToday(record.checkInTime), |
|
|
|
'is-weekend': isWeekend(record.checkInTime) |
|
|
|
}" |
|
|
|
> |
|
|
|
{{ formatWeekday(record.checkInTime) }} |
|
|
|
</div> |
|
|
|
<!-- 今天标识 --> |
|
|
|
<div v-if="isToday(record.checkInTime)" class="today-badge"> |
|
|
|
{{ $t('checkin.record.today') }} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="record-content"> |
|
|
|
<div class="record-times"> |
|
|
|
<!-- 上班打卡 --> |
|
|
|
<div class="time-item"> |
|
|
|
<div class="time-label"> |
|
|
|
{{ $t('checkin.record.time.morning') }} |
|
|
|
</div> |
|
|
|
<div class="time-value"> |
|
|
|
<span |
|
|
|
v-if="record.clockInStatus == '0'" |
|
|
|
class="status-text status-none" |
|
|
|
> |
|
|
|
{{ $t('checkin.record.status.notChecked') }} |
|
|
|
</span> |
|
|
|
<span |
|
|
|
v-else-if="record.clockInStatus == '1'" |
|
|
|
class="status-text status-normal" |
|
|
|
> |
|
|
|
{{ $t('checkin.record.status.normal') }} |
|
|
|
{{ formatTime(record.clockIn) }} |
|
|
|
</span> |
|
|
|
<span |
|
|
|
v-else-if="record.clockInStatus == '3'" |
|
|
|
class="status-text status-late" |
|
|
|
> |
|
|
|
{{ $t('checkin.record.status.late') }} |
|
|
|
{{ formatTime(record.clockIn) }} |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 下班打卡 --> |
|
|
|
<div class="time-item"> |
|
|
|
<div class="time-label"> |
|
|
|
{{ $t('checkin.record.time.evening') }} |
|
|
|
</div> |
|
|
|
<div class="time-value"> |
|
|
|
<span |
|
|
|
v-if="record.clockOutStatus == '0'" |
|
|
|
class="status-text status-none" |
|
|
|
> |
|
|
|
{{ $t('checkin.record.status.notChecked') }} |
|
|
|
</span> |
|
|
|
<span |
|
|
|
v-else-if="record.clockOutStatus == '2'" |
|
|
|
class="status-text status-normal" |
|
|
|
> |
|
|
|
{{ $t('checkin.record.status.normal') }} |
|
|
|
{{ formatTime(record.clockOut) }} |
|
|
|
</span> |
|
|
|
<span |
|
|
|
v-else-if="record.clockOutStatus == '4'" |
|
|
|
class="status-text status-early" |
|
|
|
> |
|
|
|
{{ $t('checkin.record.status.early') }} |
|
|
|
{{ formatTime(record.clockOut) }} |
|
|
|
</span> |
|
|
|
<span |
|
|
|
v-else-if="record.clockOutStatus == '5'" |
|
|
|
class="status-text status-update" |
|
|
|
> |
|
|
|
{{ $t('checkin.record.status.update') }} |
|
|
|
{{ formatTime(record.clockOut) }} |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 备注 --> |
|
|
|
<div v-if="record.description" class="record-remark"> |
|
|
|
<span class="remark-label">{{ |
|
|
|
$t('checkin.record.remark.label') |
|
|
|
}}</span> |
|
|
|
<span class="remark-text">{{ record.description }}</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="footer--position"></div> |
|
|
|
<div class="footer"> |
|
|
|
<router-link to="/m/checkin" class="footer-item"> |
|
|
|
<i class="el-icon-location-information" style="font-size: 20px;" /> |
|
|
|
<span>{{$t('checkin.button.checkedInText')}}</span> |
|
|
|
</router-link> |
|
|
|
<router-link |
|
|
|
to="/m/checkin/record" |
|
|
|
class="footer-item active" |
|
|
|
> |
|
|
|
<i class="el-icon-pie-chart" /> |
|
|
|
<span>{{$t('checkin.button.recordText')}}</span> |
|
|
|
</router-link> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script> |
|
|
|
import { mapGetters } from 'vuex' |
|
|
|
import { queryList } from '@/api/oa/attendance/history' |
|
|
|
import { formatTime, formatDate } from '@/utils/filters' |
|
|
|
import moment from 'moment' |
|
|
|
|
|
|
|
export default { |
|
|
|
name: 'CheckinRecord', |
|
|
|
data() { |
|
|
|
return { |
|
|
|
// 加载状态 |
|
|
|
loading: true, |
|
|
|
// 选中的月份 |
|
|
|
selectedMonth: 'current', |
|
|
|
// 考勤记录列表 |
|
|
|
attendanceList: [], |
|
|
|
// 查询参数 |
|
|
|
queryParams: { |
|
|
|
pageNum: 1, |
|
|
|
pageSize: 100, |
|
|
|
sysUserName: undefined, |
|
|
|
strDay: undefined, |
|
|
|
flag: 'all' |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
computed: { |
|
|
|
...mapGetters(['userinfo']), |
|
|
|
|
|
|
|
/** |
|
|
|
* 月份选项 |
|
|
|
* @returns {Array} 月份选项数组 |
|
|
|
*/ |
|
|
|
monthOptions() { |
|
|
|
return [ |
|
|
|
{ labelKey: 'checkin.record.monthFilter.current', value: 'current' }, |
|
|
|
{ labelKey: 'checkin.record.monthFilter.previous', value: 'previous' } |
|
|
|
] |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 统计数据计算 |
|
|
|
* @returns {Object} 统计数据对象 |
|
|
|
*/ |
|
|
|
statisticsData() { |
|
|
|
return this.calculateStatistics(this.attendanceList) |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 获取查询日期范围 |
|
|
|
* @returns {Object} 开始和结束日期 |
|
|
|
*/ |
|
|
|
dateRange() { |
|
|
|
if (this.selectedMonth === 'current') { |
|
|
|
const now = moment() |
|
|
|
return { |
|
|
|
start: now.clone().startOf('month').format('YYYY-MM-DD'), |
|
|
|
end: now.clone().endOf('month').format('YYYY-MM-DD') |
|
|
|
} |
|
|
|
} else { |
|
|
|
const lastMonth = moment().subtract(1, 'month') |
|
|
|
return { |
|
|
|
start: lastMonth.clone().startOf('month').format('YYYY-MM-DD'), |
|
|
|
end: lastMonth.clone().endOf('month').format('YYYY-MM-DD') |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
async mounted() { |
|
|
|
try { |
|
|
|
await this.loadAttendanceData() |
|
|
|
} catch (error) { |
|
|
|
this.handleError(this.$t('checkin.error.initFailed'), error) |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
methods: { |
|
|
|
/** |
|
|
|
* 处理月份切换 |
|
|
|
* @param {string} month 月份值 |
|
|
|
*/ |
|
|
|
async handleMonthChange(month) { |
|
|
|
if (this.selectedMonth === month) return |
|
|
|
|
|
|
|
this.selectedMonth = month |
|
|
|
await this.loadAttendanceData() |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 加载考勤数据 |
|
|
|
*/ |
|
|
|
async loadAttendanceData() { |
|
|
|
try { |
|
|
|
this.loading = true |
|
|
|
this.queryParams.sysUserName = this.userinfo.userName |
|
|
|
|
|
|
|
// 根据选中月份设置查询日期 |
|
|
|
const { start, end } = this.dateRange |
|
|
|
// this.queryParams.strDay = start |
|
|
|
// this.queryParams.endDay = end |
|
|
|
|
|
|
|
const response = await queryList(this.queryParams) |
|
|
|
|
|
|
|
// 过滤指定月份的数据 |
|
|
|
this.attendanceList = response.rows.filter((record) => { |
|
|
|
if (!record.checkInTime) return false |
|
|
|
const recordDate = moment(record.checkInTime) |
|
|
|
return recordDate.isBetween(start, end, 'day', '[]') |
|
|
|
}) |
|
|
|
} catch (error) { |
|
|
|
this.handleError(this.$t('checkin.error.loadDataFailed'), error) |
|
|
|
this.attendanceList = [] |
|
|
|
} finally { |
|
|
|
this.loading = false |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 格式化日期 - 日 |
|
|
|
* @param {string} dateStr 日期字符串 |
|
|
|
* @returns {string} 格式化后的日 |
|
|
|
*/ |
|
|
|
formatDateDay(dateStr) { |
|
|
|
if (!dateStr) return '--' |
|
|
|
return moment(dateStr).format('DD') |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 格式化日期 - 月 |
|
|
|
* @param {string} dateStr 日期字符串 |
|
|
|
* @returns {string} 格式化后的月 |
|
|
|
*/ |
|
|
|
formatDateMonth(dateStr) { |
|
|
|
if (!dateStr) return '--' |
|
|
|
const currentLang = this.$i18n.locale |
|
|
|
if (currentLang === 'en') { |
|
|
|
return moment(dateStr).format('MMM') |
|
|
|
} else if (currentLang === 'ja') { |
|
|
|
return moment(dateStr).format('MM月') |
|
|
|
} else { |
|
|
|
return moment(dateStr).format('MM月') |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 格式化星期 |
|
|
|
* @param {string} dateStr 日期字符串 |
|
|
|
* @returns {string} 星期 |
|
|
|
*/ |
|
|
|
formatWeekday(dateStr) { |
|
|
|
if (!dateStr) return '--' |
|
|
|
const dayOfWeek = moment(dateStr).day() |
|
|
|
const weekdayKeys = [ |
|
|
|
'sunday', |
|
|
|
'monday', |
|
|
|
'tuesday', |
|
|
|
'wednesday', |
|
|
|
'thursday', |
|
|
|
'friday', |
|
|
|
'saturday' |
|
|
|
] |
|
|
|
return this.$t(`checkin.record.weekdays.${weekdayKeys[dayOfWeek]}`) |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 判断是否为今天 |
|
|
|
* @param {string} dateStr 日期字符串 |
|
|
|
* @returns {boolean} 是否为今天 |
|
|
|
*/ |
|
|
|
isToday(dateStr) { |
|
|
|
if (!dateStr) return false |
|
|
|
return moment(dateStr).isSame(moment(), 'day') |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 判断是否为周末 |
|
|
|
* @param {string} dateStr 日期字符串 |
|
|
|
* @returns {boolean} 是否为周末 |
|
|
|
*/ |
|
|
|
isWeekend(dateStr) { |
|
|
|
if (!dateStr) return false |
|
|
|
const dayOfWeek = moment(dateStr).day() |
|
|
|
return dayOfWeek === 0 || dayOfWeek === 6 // 0是周日,6是周六 |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 格式化时间 |
|
|
|
* @param {string} timeStr 时间字符串 |
|
|
|
* @returns {string} 格式化后的时间 |
|
|
|
*/ |
|
|
|
formatTime(timeStr) { |
|
|
|
return formatTime(timeStr) |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 计算统计数据 |
|
|
|
* @param {Array} attendanceList 考勤记录列表 |
|
|
|
* @returns {Object} 统计结果 |
|
|
|
*/ |
|
|
|
calculateStatistics(attendanceList) { |
|
|
|
const statistics = { |
|
|
|
totalDays: 0, |
|
|
|
lateDays: 0, |
|
|
|
earlyDays: 0, |
|
|
|
normalDays: 0 |
|
|
|
} |
|
|
|
|
|
|
|
if (!attendanceList || attendanceList.length === 0) { |
|
|
|
return statistics |
|
|
|
} |
|
|
|
|
|
|
|
attendanceList.forEach((record) => { |
|
|
|
// 统计打卡天数(有上班打卡记录的天数) |
|
|
|
if (record.clockInStatus && record.clockInStatus !== '0') { |
|
|
|
statistics.totalDays++ |
|
|
|
} |
|
|
|
|
|
|
|
// 统计迟到次数(上班状态为3) |
|
|
|
if (record.clockInStatus === '3') { |
|
|
|
statistics.lateDays++ |
|
|
|
} |
|
|
|
|
|
|
|
// 统计早退次数(下班状态为4) |
|
|
|
if (record.clockOutStatus === '4') { |
|
|
|
statistics.earlyDays++ |
|
|
|
} |
|
|
|
|
|
|
|
// 统计正常天数(上班正常且下班正常) |
|
|
|
if (record.clockInStatus === '1' && record.clockOutStatus === '2') { |
|
|
|
statistics.normalDays++ |
|
|
|
} |
|
|
|
}) |
|
|
|
|
|
|
|
return statistics |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 统一错误处理 |
|
|
|
* @param {string} message 错误消息 |
|
|
|
* @param {Error} error 错误对象 |
|
|
|
*/ |
|
|
|
handleError(message, error) { |
|
|
|
console.error(message, error) |
|
|
|
this.$toast.error(error.message || message) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
</script> |
|
|
|
|
|
|
|
<style lang="scss" scoped> |
|
|
|
.page { |
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
|
|
min-height: 100vh; |
|
|
|
color: #333; |
|
|
|
@media (min-width: 480px) { |
|
|
|
width: 480px; |
|
|
|
margin: 0 auto; |
|
|
|
border-radius: 10px; |
|
|
|
} |
|
|
|
} |
|
|
|
.footer--position{ |
|
|
|
height: 60px; |
|
|
|
} |
|
|
|
.footer { |
|
|
|
position: fixed; |
|
|
|
bottom: 0; |
|
|
|
left: 0; |
|
|
|
right: 0; |
|
|
|
padding: 10px; |
|
|
|
text-align: center; |
|
|
|
background: white; |
|
|
|
z-index: 10; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
border-top: 1px solid #e5e5e5; |
|
|
|
.footer-item { |
|
|
|
flex: 1; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
cursor: pointer; |
|
|
|
flex-direction: column; |
|
|
|
&.active { |
|
|
|
color: #2196f3; |
|
|
|
} |
|
|
|
i { |
|
|
|
font-size: 18px; |
|
|
|
margin-bottom: 5px; |
|
|
|
} |
|
|
|
span { |
|
|
|
font-size: 12px; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.container { |
|
|
|
margin: 0 auto; |
|
|
|
background: #f5f5f5; |
|
|
|
min-height: 100vh; |
|
|
|
position: relative; |
|
|
|
user-select: none; |
|
|
|
} |
|
|
|
|
|
|
|
// 头部用户信息 |
|
|
|
.header { |
|
|
|
padding: 20px; |
|
|
|
background: white; |
|
|
|
border-radius: 0 0 20px 20px; |
|
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
|
|
|
} |
|
|
|
|
|
|
|
.user-info { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: space-between; |
|
|
|
} |
|
|
|
|
|
|
|
.user-left { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
} |
|
|
|
|
|
|
|
.avatar { |
|
|
|
width: 50px; |
|
|
|
height: 50px; |
|
|
|
background: #2196f3; |
|
|
|
border-radius: 12px; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
color: white; |
|
|
|
font-size: 18px; |
|
|
|
font-weight: bold; |
|
|
|
margin-right: 15px; |
|
|
|
|
|
|
|
.el-avatar { |
|
|
|
background: #2196f3; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.user-name { |
|
|
|
font-size: 20px; |
|
|
|
font-weight: 600; |
|
|
|
color: #333; |
|
|
|
} |
|
|
|
|
|
|
|
.user-department { |
|
|
|
font-size: 14px; |
|
|
|
color: #666; |
|
|
|
text-align: right; |
|
|
|
} |
|
|
|
|
|
|
|
// 月份筛选 |
|
|
|
.filter-section { |
|
|
|
padding: 15px 20px; |
|
|
|
background: white; |
|
|
|
margin-top: 10px; |
|
|
|
} |
|
|
|
|
|
|
|
.month-tabs { |
|
|
|
display: flex; |
|
|
|
background: #f8f9fa; |
|
|
|
border-radius: 8px; |
|
|
|
padding: 4px; |
|
|
|
} |
|
|
|
|
|
|
|
.month-tab { |
|
|
|
flex: 1; |
|
|
|
text-align: center; |
|
|
|
padding: 10px 0; |
|
|
|
border-radius: 6px; |
|
|
|
font-size: 14px; |
|
|
|
font-weight: 500; |
|
|
|
color: #666; |
|
|
|
cursor: pointer; |
|
|
|
transition: all 0.3s ease; |
|
|
|
|
|
|
|
&.active { |
|
|
|
background: #2196f3; |
|
|
|
color: white; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 统计信息区域 |
|
|
|
.statistics-section { |
|
|
|
padding: 10px 20px; |
|
|
|
background: white; |
|
|
|
margin-top: 10px; |
|
|
|
} |
|
|
|
|
|
|
|
.statistics-cards { |
|
|
|
display: grid; |
|
|
|
grid-template-columns: repeat(4, 1fr); |
|
|
|
gap: 12px; |
|
|
|
} |
|
|
|
|
|
|
|
.statistics-card { |
|
|
|
background: #f8f9fa; |
|
|
|
border-radius: 8px; |
|
|
|
padding: 12px 8px; |
|
|
|
text-align: center; |
|
|
|
transition: all 0.3s ease; |
|
|
|
|
|
|
|
&:active { |
|
|
|
transform: scale(0.95); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.statistics-number { |
|
|
|
font-size: 20px; |
|
|
|
font-weight: 600; |
|
|
|
color: #333; |
|
|
|
margin-bottom: 4px; |
|
|
|
line-height: 1; |
|
|
|
|
|
|
|
&.late { |
|
|
|
color: #f56c6c; |
|
|
|
} |
|
|
|
|
|
|
|
&.early { |
|
|
|
color: #e6a23c; |
|
|
|
} |
|
|
|
|
|
|
|
&.normal { |
|
|
|
color: #67c23a; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.statistics-label { |
|
|
|
font-size: 12px; |
|
|
|
color: #666; |
|
|
|
font-weight: 500; |
|
|
|
} |
|
|
|
|
|
|
|
// 记录列表区域 |
|
|
|
.record-section { |
|
|
|
padding: 10px 0; |
|
|
|
} |
|
|
|
|
|
|
|
.loading-wrapper { |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
padding: 60px 20px; |
|
|
|
color: #666; |
|
|
|
|
|
|
|
i { |
|
|
|
font-size: 24px; |
|
|
|
margin-bottom: 10px; |
|
|
|
} |
|
|
|
|
|
|
|
span { |
|
|
|
font-size: 14px; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.empty-wrapper { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
padding: 60px 20px; |
|
|
|
} |
|
|
|
|
|
|
|
.empty-text { |
|
|
|
font-size: 16px; |
|
|
|
color: #999; |
|
|
|
} |
|
|
|
|
|
|
|
.record-list { |
|
|
|
padding: 0 15px; |
|
|
|
} |
|
|
|
|
|
|
|
.record-item { |
|
|
|
display: flex; |
|
|
|
background: white; |
|
|
|
border-radius: 12px; |
|
|
|
padding: 12px; |
|
|
|
margin-bottom: 12px; |
|
|
|
transition: all 0.3s ease; |
|
|
|
position: relative; |
|
|
|
|
|
|
|
&:active { |
|
|
|
transform: scale(0.98); |
|
|
|
} |
|
|
|
|
|
|
|
// 今天的记录特殊样式 |
|
|
|
&.is-today { |
|
|
|
background: linear-gradient(135deg, #fff9e6, #ffffff); |
|
|
|
} |
|
|
|
|
|
|
|
// 周末的记录特殊样式 |
|
|
|
&.is-weekend { |
|
|
|
background: linear-gradient(135deg, #f0f8ff, #ffffff); |
|
|
|
} |
|
|
|
|
|
|
|
// 今天且是周末的组合样式 |
|
|
|
&.is-today.is-weekend { |
|
|
|
background: linear-gradient(135deg, #fff0f5, #ffffff); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.record-date { |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
width: 60px; |
|
|
|
margin-right: 16px; |
|
|
|
flex-shrink: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.date-day { |
|
|
|
font-size: 24px; |
|
|
|
font-weight: 600; |
|
|
|
color: #333; |
|
|
|
line-height: 1; |
|
|
|
} |
|
|
|
|
|
|
|
.date-month { |
|
|
|
font-size: 12px; |
|
|
|
color: #666; |
|
|
|
margin: 2px 0; |
|
|
|
} |
|
|
|
|
|
|
|
.date-weekday { |
|
|
|
font-size: 10px; |
|
|
|
color: #999; |
|
|
|
background: #f0f0f0; |
|
|
|
padding: 2px 6px; |
|
|
|
border-radius: 4px; |
|
|
|
transition: all 0.3s ease; |
|
|
|
|
|
|
|
&.is-today { |
|
|
|
background: #ffd700; |
|
|
|
color: #333; |
|
|
|
font-weight: 600; |
|
|
|
} |
|
|
|
|
|
|
|
&.is-weekend { |
|
|
|
background: #87ceeb; |
|
|
|
color: white; |
|
|
|
font-weight: 500; |
|
|
|
} |
|
|
|
|
|
|
|
&.is-today.is-weekend { |
|
|
|
background: #ff69b4; |
|
|
|
color: white; |
|
|
|
font-weight: 600; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.today-badge { |
|
|
|
font-size: 8px; |
|
|
|
color: #ff6b35; |
|
|
|
background: linear-gradient(135deg, #fff2e6, #ffe6d9); |
|
|
|
padding: 2px 4px; |
|
|
|
border-radius: 6px; |
|
|
|
margin-top: 4px; |
|
|
|
text-align: center; |
|
|
|
border: 1px solid #ffb088; |
|
|
|
animation: pulse 2s infinite; |
|
|
|
} |
|
|
|
|
|
|
|
@keyframes pulse { |
|
|
|
0% { |
|
|
|
box-shadow: 0 0 0 0 rgba(255, 107, 53, 0.4); |
|
|
|
} |
|
|
|
70% { |
|
|
|
box-shadow: 0 0 0 6px rgba(255, 107, 53, 0); |
|
|
|
} |
|
|
|
100% { |
|
|
|
box-shadow: 0 0 0 0 rgba(255, 107, 53, 0); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.record-content { |
|
|
|
flex: 1; |
|
|
|
min-width: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.record-group { |
|
|
|
font-size: 14px; |
|
|
|
color: #666; |
|
|
|
margin-bottom: 8px; |
|
|
|
font-weight: 500; |
|
|
|
} |
|
|
|
|
|
|
|
.record-times { |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
gap: 6px; |
|
|
|
} |
|
|
|
|
|
|
|
.time-item { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: space-between; |
|
|
|
} |
|
|
|
|
|
|
|
.time-label { |
|
|
|
font-size: 13px; |
|
|
|
color: #333; |
|
|
|
font-weight: 500; |
|
|
|
width: 80px; |
|
|
|
flex-shrink: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.time-value { |
|
|
|
flex: 1; |
|
|
|
text-align: right; |
|
|
|
} |
|
|
|
|
|
|
|
.status-text { |
|
|
|
font-size: 12px; |
|
|
|
font-weight: 500; |
|
|
|
padding: 2px 8px; |
|
|
|
border-radius: 4px; |
|
|
|
|
|
|
|
&.status-none { |
|
|
|
color: #909399; |
|
|
|
background: #f4f4f5; |
|
|
|
} |
|
|
|
|
|
|
|
&.status-normal { |
|
|
|
color: #409eff; |
|
|
|
background: #ecf5ff; |
|
|
|
} |
|
|
|
|
|
|
|
&.status-late { |
|
|
|
color: #f56c6c; |
|
|
|
background: #fef0f0; |
|
|
|
} |
|
|
|
|
|
|
|
&.status-early { |
|
|
|
color: #e6a23c; |
|
|
|
background: #fdf6ec; |
|
|
|
} |
|
|
|
|
|
|
|
&.status-update { |
|
|
|
color: #67c23a; |
|
|
|
background: #f0f9ff; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.record-remark { |
|
|
|
margin-top: 8px; |
|
|
|
padding-top: 8px; |
|
|
|
border-top: 1px solid #f0f0f0; |
|
|
|
font-size: 12px; |
|
|
|
} |
|
|
|
|
|
|
|
.remark-label { |
|
|
|
color: #666; |
|
|
|
} |
|
|
|
|
|
|
|
.remark-text { |
|
|
|
color: #333; |
|
|
|
margin-left: 4px; |
|
|
|
} |
|
|
|
</style> |