first commit

This commit is contained in:
2026-02-06 20:21:10 +08:00
commit f9d7f980bb
3994 changed files with 1146030 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+880
View File
@@ -0,0 +1,880 @@
<template>
<view class="statistics-container">
<!-- 使用子头部组件 -->
<sub-header
title="考勤统计"
@goBack="goBack"
@showMoreOptions="showMoreOptions"
/>
<!-- 页面内容 -->
<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>
import SubHeader from '../../components/subHeader.vue'
export default {
components: {
SubHeader
},
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>
.statistics-container {
background: #f7f8fa;
min-height: 100vh;
padding-top: calc(var(--status-bar-height) + 88rpx);
}
/* 支持安全区域的设备 */
@supports (padding: max(0px)) {
.statistics-container {
padding-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
}
}
.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>