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

994 lines
26 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="statistics-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>
<!-- 页面内容 -->
<scroll-view scroll-y class="statistics-content">
<!-- 导出报表 -->
<view class="export-card card">
<view class="export-title">导出报表</view>
<view class="exports">
<view class="btn-export" @click="exportReport"> 导出 </view>
<i class="fas fa-angle-right" style="font-size: 20rpx; color: var(--tab-inactive);position: relative;top: 4rpx;"></i>
</view>
</view>
<!-- 统计数据 -->
<view class="stats-card card">
<!-- Tabbar -->
<view class="stat-tabs">
<view
v-for="t in tabs"
:key="t.value"
class="stat-tab"
:class="{ active: statTab === t.value }"
@click="statTab = t.value"
>{{ t.label }}</view
>
</view>
<!-- 日统计显示日历 -->
<view v-if="statTab === 'day'" class="tab-content">
<view class="calendar">
<view class="cal-title">
{{ today.getFullYear() }}{{ today.getMonth() + 1 }}
</view>
<view class="cal-week-head">
<text
v-for="w in ['日', '一', '二', '三', '四', '五', '六']"
:key="w"
>{{ w }}</text
>
</view>
<view class="cal-body">
<view
v-for="(week, weekIndex) in calendarWeeks"
:key="weekIndex"
class="cal-row"
>
<view
v-for="day in week"
:key="day.key"
class="cal-cell"
:class="{
empty: day.empty,
selected:
!day.empty && calendarSelected.includes(day.fullDate),
}"
@click="
!day.empty &&
selectDay({
fullDate: day.fullDate,
day: day.day,
month: day.month,
year: day.year,
})
"
>{{ day.empty ? "" : day.day }}</view
>
</view>
</view>
</view>
<!-- 选中日期的数据显示卡片 -->
<view v-if="selectedDayInfo" class="day-data-card">
<view class="day-data-header">
<view class="day-data-title">
<i class="fas fa-calendar-day"></i>
<text>{{ selectedDayInfo.year }}{{ selectedDayInfo.month }}{{ selectedDayInfo.day }}</text>
</view>
<view class="day-data-subtitle">考勤详情</view>
</view>
<view class="day-data-content">
<view
class="day-data-item"
v-for="(item, index) in dailyStats"
:key="index"
>
<view class="day-data-icon" :class="item.status || 'default'">
<i :class="item.icon"></i>
</view>
<view class="day-data-info">
<view class="day-data-label">{{ item.title }}</view>
<view class="day-data-value" :class="item.status || 'default'">
{{ item.value }}
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 周统计 -->
<view v-if="statTab === 'week'" class="tab-content">
<view class="bar-tabs">
<view
class="bar-tab"
v-for="(item, index) in weekTabs"
:key="item.value"
:class="{ active: weekTab === item.value }"
@click="weekTab = item.value"
>{{ item.label }}</view
>
</view>
<view class="count-list">
<view
class="count-item"
v-for="(item, idx) in (weekStats[weekTab] || [])"
:key="idx"
>
<view class="c-value">{{ item.value }}</view>
<view class="c-title">{{ item.title }}</view>
</view>
</view>
</view>
<!-- 月统计 -->
<view v-if="statTab === 'month'" class="tab-content">
<view class="month-grid">
<view
class="month-item"
v-for="(item, index) in monthTabs"
:key="item.value"
:class="{ active: monthTab === item.value }"
@click="monthTab = item.value"
>
<view class="month-label">{{ item.label }}</view>
</view>
</view>
<view class="count-list">
<view
class="count-item"
v-for="(item, idx) in (monthStats[monthTab] || [])"
:key="idx"
>
<view class="c-value">{{ item.value }}</view>
<view class="c-title">{{ item.title }}</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
const today = new Date();
return {
today: today,
tabs: [
{ label: "日统计", value: "day" },
{ label: "周统计", value: "week" },
{ label: "月统计", value: "month" },
],
statTab: "day",
calendarSelected: [
`${today.getFullYear()}-${String(today.getMonth() + 1).padStart(
2,
"0"
)}-${String(today.getDate()).padStart(2, "0")}`,
],
weekTabs: [
{ label: "第1周", value: "1" },
{ label: "第2周", value: "2" },
{ label: "第3周", value: "3" },
{ label: "第4周", value: "4" },
],
weekTab: "1",
monthTabs: Array.from({ length: 12 }, (_, i) => ({
label: `${i + 1}`,
value: String(i + 1),
})),
monthTab: String(today.getMonth() + 1),
baseStat: [
{ title: "平均工时", value: "7.8h" },
{ title: "迟到次数", value: "1" },
{ title: "早退次数", value: "0" },
{ title: "缺卡次数", value: "0" },
{ title: "旷工次数", value: "0" },
{ title: "外勤次数", value: "2" },
{ title: "加班时长", value: "4h" },
{ title: "调休时长", value: "2h" },
],
selectedDayData: null, // 选中日期的数据
selectedDayInfo: null, // 选中日期的基本信息
};
},
computed: {
weekStats() {
try {
return {
"1": this.baseStat || [],
"2": (this.baseStat || []).map((item, i) =>
i === 1 ? { ...item, value: "0" } : item
),
"3": (this.baseStat || []).map((item, i) =>
i === 3 ? { ...item, value: "1" } : item
),
"4": (this.baseStat || []).map((item, i) =>
i === 5 ? { ...item, value: "3" } : item
),
};
} catch (error) {
console.error('周统计数据生成错误:', error);
return {};
}
},
monthStats() {
try {
const stats = {};
(this.monthTabs || []).forEach((_, i) => {
stats[String(i + 1)] = (this.baseStat || []).map((item) => ({
...item,
value: String(
Math.floor(Math.random() * 3) +
(item.value.includes("h") ? "h" : "")
),
}));
});
return stats;
} catch (error) {
console.error('月统计数据生成错误:', error);
return {};
}
},
// 日历相关计算属性
days() {
const d = new Date(
this.today.getFullYear(),
this.today.getMonth() + 1,
0
).getDate();
return Array.from({ length: d }, (_, i) => i + 1);
},
firstDay() {
return new Date(
this.today.getFullYear(),
this.today.getMonth(),
1
).getDay();
},
calendarWeeks() {
try {
const year = this.today.getFullYear();
const month = this.today.getMonth();
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const weeks = [];
let currentWeek = [];
// 添加空白日期(上个月的末尾几天)
for (let i = 0; i < firstDay; i++) {
currentWeek.push({
key: `empty-${i}`,
empty: true,
day: "",
fullDate: "",
month: month,
year: year,
});
}
// 添加当前月的日期
for (let day = 1; day <= daysInMonth; day++) {
const fullDate = `${year}-${String(month + 1).padStart(
2,
"0"
)}-${String(day).padStart(2, "0")}`;
currentWeek.push({
key: `day-${day}`,
empty: false,
day: day,
fullDate: fullDate,
month: month + 1,
year: year,
});
// 如果一周满了7天开始新的一周
if (currentWeek.length === 7) {
weeks.push([...currentWeek]);
currentWeek = [];
}
}
// 如果最后一周不满7天用空白日期填充
while (currentWeek.length > 0 && currentWeek.length < 7) {
currentWeek.push({
key: `empty-end-${currentWeek.length}`,
empty: true,
day: "",
fullDate: "",
month: month,
year: year,
});
}
// 如果还有未完成的周,添加到结果中
if (currentWeek.length > 0) {
weeks.push(currentWeek);
}
return weeks;
} catch (error) {
console.error('日历生成错误:', error);
return [];
}
},
isMobile() {
return getApp().globalData.isMobile;
},
// 生成每日数据
dailyStats() {
if (!this.selectedDayInfo) return [];
const { year, month, day } = this.selectedDayInfo;
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
// 模拟不同日期的不同数据
const randomSeed = year * 10000 + month * 100 + day;
const random = (seed) => {
const x = Math.sin(seed) * 10000;
return x - Math.floor(x);
};
return [
{
title: "上班时间",
value: `${String(Math.floor(random(randomSeed) * 2) + 8).padStart(2, '0')}:${String(Math.floor(random(randomSeed + 1) * 60)).padStart(2, '0')}`,
icon: "fas fa-clock"
},
{
title: "下班时间",
value: `${String(Math.floor(random(randomSeed + 2) * 2) + 17).padStart(2, '0')}:${String(Math.floor(random(randomSeed + 3) * 60)).padStart(2, '0')}`,
icon: "fas fa-clock"
},
{
title: "迟到状态",
value: random(randomSeed + 5) > 0.8 ? "迟到" : "正常",
icon: random(randomSeed + 5) > 0.8 ? "fas fa-exclamation-triangle" : "fas fa-check-circle",
status: random(randomSeed + 5) > 0.8 ? "warning" : "success"
},
{
title: "早退状态",
value: random(randomSeed + 6) > 0.9 ? "早退" : "正常",
icon: random(randomSeed + 6) > 0.9 ? "fas fa-exclamation-triangle" : "fas fa-check-circle",
status: random(randomSeed + 6) > 0.9 ? "warning" : "success"
},
{
title: "工作时长",
value: `${(random(randomSeed + 4) * 4 + 6).toFixed(1)}h`,
icon: "fas fa-hourglass-half"
},
{
title: "外勤次数",
value: Math.floor(random(randomSeed + 7) * 3).toString(),
icon: "fas fa-map-marker-alt"
},
{
title: "加班时长",
value: random(randomSeed + 8) > 0.6 ? `${(random(randomSeed + 8) * 3).toFixed(1)}h` : "0h",
icon: "fas fa-moon"
},
{
title: "调休时长",
value: random(randomSeed + 9) > 0.7 ? `${(random(randomSeed + 9) * 2).toFixed(1)}h` : "0h",
icon: "fas fa-calendar-alt"
},
];
},
},
mounted() {
console.log('统计页面初始化');
console.log('当前月份:', this.today.getMonth() + 1);
console.log('基础数据:', this.baseStat);
console.log('周标签:', this.weekTabs);
console.log('月标签:', this.monthTabs);
console.log('当前周标签值:', this.weekTab, typeof this.weekTab);
console.log('当前月标签值:', this.monthTab, typeof this.monthTab);
console.log('周统计数据:', this.weekStats);
console.log('月统计数据:', this.monthStats);
console.log('周统计键:', Object.keys(this.weekStats));
console.log('月统计键:', Object.keys(this.monthStats));
console.log('周统计访问测试:', this.weekStats[this.weekTab]);
console.log('月统计访问测试:', this.monthStats[this.monthTab]);
// 初始化默认选中今天
this.selectedDayInfo = {
year: this.today.getFullYear(),
month: this.today.getMonth() + 1,
day: this.today.getDate(),
fullDate: `${this.today.getFullYear()}-${String(this.today.getMonth() + 1).padStart(2, '0')}-${String(this.today.getDate()).padStart(2, '0')}`
};
console.log('默认选中今天:', this.selectedDayInfo);
},
methods: {
selectDay(day) {
this.calendarSelected = [day.fullDate];
this.selectedDayInfo = {
year: day.year,
month: day.month,
day: day.day,
fullDate: day.fullDate
};
console.log('选择日期:', day);
console.log('选中日期信息:', this.selectedDayInfo);
},
exportReport() {
uni.showToast({ title: "导出功能暂未开放" });
},
goBack() {
uni.navigateBack();
},
showMoreOptions() {
uni.showActionSheet({
itemList: ['刷新数据', '导出报表', '设置'],
success: (res) => {
if (res.tapIndex === 0) {
uni.showToast({ title: '刷新成功' });
} else if (res.tapIndex === 1) {
this.exportReport();
} else if (res.tapIndex === 2) {
uni.showToast({ title: '设置功能暂未开放' });
}
}
});
},
},
};
</script>
<style lang="scss" scoped>
/* 移动设备顶部状态栏 */
.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 + .statistics-content {
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);
}
/* 移动设备下的导航栏样式 */
.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;
}
/* 移动设备下的按钮悬停效果 */
.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);
}
.header-icon {
font-size: 36rpx;
color: var(--text-color);
padding: 10rpx;
transition: all 0.3s ease;
}
.header-icon:active {
background: var(--hover-bg);
transform: scale(0.95);
border-radius: 8rpx;
}
/* 移动设备下为内容添加顶部间距 */
.top_bar + .statistics-content {
margin-top: calc(var(--status-bar-height) + 88rpx);
}
/* 浏览器环境下为内容添加顶部间距 */
.chat-header + .statistics-content {
margin-top: calc(var(--status-bar-height) + 88rpx);
}
.statistics-container {
background: #f7f8fa;
min-height: 100vh;
}
.statistics-content {
padding: 32rpx 0;
min-height: calc(100vh - 88rpx);
}
.card {
background: #fff;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx 0 rgba(0, 0, 0, 0.04);
margin: 0 32rpx 32rpx;
padding: 32rpx;
}
.export-card {
display: flex;
justify-content: space-between;
align-items: center;
.export-title {
font-size: 32rpx;
font-weight: 600;
}
.btn-export {
// background: #3c9cff;
color: var(--tab-inactive);
font-size: 28rpx;
border: none;
// padding: 4rpx 40rpx;
border-radius: 8rpx;
}
.exports {
display: flex;
align-items: center;
.btn-export {
margin-right: 16rpx;
}
}
}
.stats-card {
.stat-tabs {
display: flex;
margin-bottom: 32rpx;
.stat-tab {
flex: 1;
text-align: center;
font-size: 28rpx;
color: var(--text-muted);
padding-bottom: 16rpx;
border-bottom: 4rpx solid transparent;
transition: all 0.2s;
&.active {
color: var(--primary-color);
font-weight: 600;
border-color: var(--primary-color);
}
}
}
.tab-content {
margin-top: 16rpx;
}
// 月统计网格布局
.month-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12rpx;
margin-bottom: 24rpx;
.month-item {
background: var(--gray-lighter);
border-radius: 12rpx;
padding: 20rpx 12rpx;
text-align: center;
border: 1rpx solid var(--border-light);
transition: all 0.3s ease;
cursor: pointer;
.month-label {
font-size: 26rpx;
color: var(--text-secondary);
font-weight: 500;
}
&.active {
background: var(--primary-color);
border-color: var(--primary-color);
box-shadow: var(--shadow-md);
transform: translateY(-2rpx);
.month-label {
color: var(--white);
font-weight: 600;
}
}
&:not(.active):active {
background: var(--primary-light);
transform: scale(0.98);
.month-label {
color: var(--white);
}
}
}
}
.bar-tabs {
display: flex;
background: var(--gray-lighter);
border-radius: 12rpx;
padding: 6rpx;
margin-bottom: 24rpx;
position: relative;
.bar-tab {
flex: 1;
text-align: center;
font-size: 28rpx;
color: var(--text-secondary);
padding: 16rpx 12rpx;
border-radius: 8rpx;
margin: 0 2rpx;
background: transparent;
transition: all 0.3s ease;
position: relative;
font-weight: 500;
&.active {
color: var(--primary-color);
background: var(--surface);
font-weight: 600;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transform: translateY(-1rpx);
}
&:not(.active):active {
background: rgba(60, 156, 255, 0.1);
}
}
}
.bar-tabs-scroll {
background: #f8f9fa;
border-radius: 12rpx;
padding: 6rpx;
margin-bottom: 24rpx;
.bar-scroll {
white-space: nowrap;
.bar-tab {
display: inline-block;
min-width: 120rpx;
margin: 0 4rpx;
padding: 16rpx 20rpx;
text-align: center;
font-size: 28rpx;
color: #666;
border-radius: 8rpx;
background: transparent;
transition: all 0.3s ease;
font-weight: 500;
&.active {
color: #3c9cff;
background: #fff;
font-weight: 600;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transform: translateY(-1rpx);
}
&:not(.active):active {
background: rgba(60, 156, 255, 0.1);
}
}
}
}
// 日统计数据显示卡片
.day-data-card {
margin-top: 24rpx;
background: var(--surface);
border-radius: 16rpx;
box-shadow: var(--shadow-md);
overflow: hidden;
border: 1rpx solid var(--border);
.day-data-header {
background: var(--gradient-primary);
padding: 32rpx 24rpx;
color: var(--white);
.day-data-title {
display: flex;
align-items: center;
font-size: 32rpx;
font-weight: 600;
margin-bottom: 8rpx;
color: var(--white);
i {
margin-right: 12rpx;
font-size: 28rpx;
color: var(--white);
}
}
.day-data-subtitle {
font-size: 24rpx;
opacity: 0.9;
color: var(--white);
}
}
.day-data-content {
padding: 24rpx;
background: var(--surface);
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.day-data-item {
display: flex;
align-items: center;
padding: 20rpx 16rpx;
width: 48%;
margin-bottom: 16rpx;
background: var(--gray-lighter);
border-radius: 12rpx;
border: 1rpx solid var(--border-light);
box-sizing: border-box;
&:nth-child(odd) {
margin-right: 2%;
}
&:nth-child(even) {
margin-left: 2%;
}
.day-data-icon {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12rpx;
flex-shrink: 0;
i {
font-size: 20rpx;
}
&.default {
background: var(--gray-light);
color: var(--text-muted);
}
&.success {
background: var(--success-light);
color: var(--success);
}
&.warning {
background: var(--warning-light);
color: var(--warning);
}
}
.day-data-info {
flex: 1;
min-width: 0;
.day-data-label {
font-size: 22rpx;
color: var(--text-secondary);
margin-bottom: 6rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.day-data-value {
font-size: 24rpx;
font-weight: 600;
&.default {
color: var(--text-color);
}
&.success {
color: var(--success);
}
&.warning {
color: var(--warning);
}
}
}
}
}
}
.count-list {
display: flex;
flex-wrap: wrap;
margin-top: 16rpx;
justify-content: space-between;
.count-item {
display: flex;
flex-direction: column;
align-items: center;
width: 48%;
margin-bottom: 16rpx;
padding: 24rpx;
background: var(--gray-lighter);
border-radius: 12rpx;
box-sizing: border-box;
border: 1rpx solid var(--border-light);
.c-title {
color: var(--text-secondary);
margin-bottom: 8rpx;
font-size: 20rpx;
}
.c-value {
color: var(--text-color);
font-size: 40rpx;
font-weight: 500;
margin-bottom: 8rpx;
}
}
}
}
/* 日历样式 */
.calendar {
background: var(--surface);
border-radius: 12rpx;
padding: 24rpx;
margin-top: 16rpx;
border: 1rpx solid var(--border);
box-shadow: var(--shadow);
.cal-title {
text-align: center;
font-weight: 600;
font-size: 32rpx;
color: var(--title-color);
margin-bottom: 24rpx;
}
.cal-week-head {
display: flex;
margin-bottom: 16rpx;
text {
flex: 1;
text-align: center;
font-size: 26rpx;
color: var(--text-muted);
font-weight: 500;
padding: 12rpx 0;
}
}
.cal-body {
.cal-row {
display: flex;
margin-bottom: 8rpx;
.cal-cell {
flex: 1;
height: 64rpx;
line-height: 64rpx;
text-align: center;
border-radius: 8rpx;
margin: 0 4rpx;
background: var(--gray-lighter);
font-size: 28rpx;
color: var(--text-color);
transition: all 0.2s ease;
position: relative;
&:active {
transform: scale(0.95);
}
&.selected {
background: var(--primary-color);
color: var(--white);
font-weight: 600;
box-shadow: var(--shadow-md);
}
&.empty {
background: transparent;
pointer-events: none;
}
&:not(.empty):not(.selected):hover {
background: var(--primary-light);
color: var(--white);
}
}
}
}
}
:deep() {
.uni-scroll-view {
overflow: hidden !important;
}
}
</style>