-
+
+
+
+
+
@@ -158,7 +155,7 @@
-
+
@@ -198,6 +195,7 @@
-
+
diff --git a/klp-wms/src/main/java/com/klp/controller/WmsMaterialCoilController.java b/klp-wms/src/main/java/com/klp/controller/WmsMaterialCoilController.java
index 4ff45e3f..d6054043 100644
--- a/klp-wms/src/main/java/com/klp/controller/WmsMaterialCoilController.java
+++ b/klp-wms/src/main/java/com/klp/controller/WmsMaterialCoilController.java
@@ -5,6 +5,9 @@ import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.Arrays;
+import java.util.Set;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.stream.Collectors;
import java.util.HashMap;
@@ -30,6 +33,7 @@ import com.klp.common.core.validate.AddGroup;
import com.klp.common.core.validate.EditGroup;
import com.klp.common.enums.BusinessType;
import com.klp.common.utils.poi.ExcelUtil;
+import com.klp.common.utils.StringUtils;
import com.klp.domain.bo.WmsMaterialCoilBo;
import com.klp.domain.bo.WmsMaterialCoilReportSummaryBo;
import com.klp.domain.vo.dashboard.CoilTrimStatisticsVo;
@@ -130,6 +134,70 @@ public class WmsMaterialCoilController extends BaseController {
List list = iWmsMaterialCoilService.queryExportList(bo);
ExcelUtil.exportExcel(list, "钢卷物料表", WmsMaterialCoilExportVo.class, response);
}
+
+ /**
+ * 个性化导出:前端传入要导出的字段名,仅导出选中列
+ * columns 参数为逗号分隔的 Java 字段名,如 "itemTypeDesc,enterCoilNo,netWeight"
+ * 不传 columns 时等同于 /exportAll 导出全部字段
+ */
+ @Log(title = "钢卷物料表", businessType = BusinessType.EXPORT)
+ @PostMapping("/exportCustom")
+ public void exportCustom(WmsMaterialCoilBo bo,
+ @RequestParam(required = false) String columns,
+ HttpServletResponse response) {
+ List list = iWmsMaterialCoilService.queryExportListAll(bo);
+ if (StringUtils.isNotBlank(columns)) {
+ Set includeFields = new HashSet<>(Arrays.asList(columns.split(",")));
+ ExcelUtil.exportExcel(list, "钢卷物料表", WmsMaterialCoilAllExportVo.class, includeFields, response);
+ } else {
+ ExcelUtil.exportExcel(list, "钢卷物料表", WmsMaterialCoilAllExportVo.class, response);
+ }
+ }
+
+ /**
+ * 获取可导出的列元数据(供前端列选择器使用)
+ * 返回 { "fieldName": "中文列名" } 的映射,基于完整导出字段
+ */
+ @GetMapping("/exportColumns")
+ public R
+
+
+
+
{
From aad568f3208cea1f4b66564b51cdf5f6f0c7adb4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=A0=82=E7=B3=96?= <2178503051@qq.com>
Date: Fri, 29 May 2026 17:05:23 +0800
Subject: [PATCH 12/13] =?UTF-8?q?feat(mes,wms):=20=E6=96=B0=E5=A2=9E?=
=?UTF-8?q?=E8=AE=BE=E5=A4=87=E9=80=81=E6=A3=80=E5=AE=A1=E6=89=B9=E6=B5=81?=
=?UTF-8?q?=E7=A8=8B=E5=8F=8A=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
1. 新增设备送检审批的API接口层
2. 在待办页面添加设备送检审批标签页
3. 完善设备巡检日报的送检提交功能
4. 修复报表模板查询的参数传递问题
5. 优化设备送检审批单的业务逻辑处理
---
...quipmentInspectionApprovalServiceImpl.java | 12 +-
.../mes/eqp/equipmentInspectionApproval.js | 39 +++
klp-ui/src/views/mes/eqp/check/approval.vue | 251 ++++++++++++++++++
klp-ui/src/views/mes/eqp/check/day.vue | 40 ++-
.../src/views/wms/report/template/action.vue | 4 +-
klp-ui/src/views/wms/todo/index.vue | 5 +
6 files changed, 340 insertions(+), 11 deletions(-)
create mode 100644 klp-ui/src/api/mes/eqp/equipmentInspectionApproval.js
create mode 100644 klp-ui/src/views/mes/eqp/check/approval.vue
diff --git a/klp-mes/src/main/java/com/klp/mes/eqp/service/impl/EqpEquipmentInspectionApprovalServiceImpl.java b/klp-mes/src/main/java/com/klp/mes/eqp/service/impl/EqpEquipmentInspectionApprovalServiceImpl.java
index 736a8993..7f9b1cdb 100644
--- a/klp-mes/src/main/java/com/klp/mes/eqp/service/impl/EqpEquipmentInspectionApprovalServiceImpl.java
+++ b/klp-mes/src/main/java/com/klp/mes/eqp/service/impl/EqpEquipmentInspectionApprovalServiceImpl.java
@@ -81,12 +81,8 @@ public class EqpEquipmentInspectionApprovalServiceImpl implements IEqpEquipmentI
@Override
public Boolean insertByBo(EqpEquipmentInspectionApprovalBo bo) {
EqpEquipmentInspectionApproval add = BeanUtil.toBean(bo, EqpEquipmentInspectionApproval.class);
- if (StringUtils.isBlank(bo.getApprovalUser())) {
- add.setApprovalUser(LoginHelper.getNickName());
- }
- if (bo.getApplyTime() == null) {
- add.setApplyTime(new Date());
- }
+ add.setApplyUser(LoginHelper.getNickName());
+ add.setApplyTime(new Date());
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
@@ -101,6 +97,10 @@ public class EqpEquipmentInspectionApprovalServiceImpl implements IEqpEquipmentI
@Override
public Boolean updateByBo(EqpEquipmentInspectionApprovalBo bo) {
EqpEquipmentInspectionApproval update = BeanUtil.toBean(bo, EqpEquipmentInspectionApproval.class);
+ if (bo.getApprovalStatus() != null && (bo.getApprovalStatus() == 2 || bo.getApprovalStatus() == 3)) {
+ update.setApprovalUser(LoginHelper.getNickName());
+ update.setApprovalTime(new Date());
+ }
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
diff --git a/klp-ui/src/api/mes/eqp/equipmentInspectionApproval.js b/klp-ui/src/api/mes/eqp/equipmentInspectionApproval.js
new file mode 100644
index 00000000..66d00011
--- /dev/null
+++ b/klp-ui/src/api/mes/eqp/equipmentInspectionApproval.js
@@ -0,0 +1,39 @@
+import request from '@/utils/request'
+
+export function listEquipmentInspectionApproval(query) {
+ return request({
+ url: '/eqp/equipmentInspectionApproval/list',
+ method: 'get',
+ params: query
+ })
+}
+
+export function getEquipmentInspectionApproval(approvalId) {
+ return request({
+ url: '/eqp/equipmentInspectionApproval/' + approvalId,
+ method: 'get'
+ })
+}
+
+export function addEquipmentInspectionApproval(data) {
+ return request({
+ url: '/eqp/equipmentInspectionApproval',
+ method: 'post',
+ data: data
+ })
+}
+
+export function updateEquipmentInspectionApproval(data) {
+ return request({
+ url: '/eqp/equipmentInspectionApproval',
+ method: 'put',
+ data: data
+ })
+}
+
+export function delEquipmentInspectionApproval(approvalId) {
+ return request({
+ url: '/eqp/equipmentInspectionApproval/' + approvalId,
+ method: 'delete'
+ })
+}
diff --git a/klp-ui/src/views/mes/eqp/check/approval.vue b/klp-ui/src/views/mes/eqp/check/approval.vue
new file mode 100644
index 00000000..1f766e11
--- /dev/null
+++ b/klp-ui/src/views/mes/eqp/check/approval.vue
@@ -0,0 +1,251 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+ {{ getLineName(scope.row.productionLine) }}
+
+
+
+
+ {{ parseTime(scope.row.insStartTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
+
+
+
+
+ {{ parseTime(scope.row.insEndTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
+
+
+
+
+
+ {{ parseTime(scope.row.applyTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
+
+
+
+
+ 待审批
+ 已通过
+ 已驳回
+ 已撤销
+
+
+
+
+
+ {{ parseTime(scope.row.approvalTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
+
+
+
+
+
+
+ 详情
+ 通过
+ 驳回
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/klp-ui/src/views/mes/eqp/check/day.vue b/klp-ui/src/views/mes/eqp/check/day.vue
index 50d425d8..511f2b2a 100644
--- a/klp-ui/src/views/mes/eqp/check/day.vue
+++ b/klp-ui/src/views/mes/eqp/check/day.vue
@@ -15,6 +15,9 @@
查询
+
+ 送检
+
@@ -107,17 +110,25 @@
import { listEquipmentPart } from "@/api/mes/eqp/equipmentPart";
import { listEquipmentInspectionRecord } from "@/api/mes/eqp/equipmentInspectionRecord";
import { listProductionLine } from "@/api/wms/productionLine";
+import { addEquipmentInspectionApproval } from "@/api/mes/eqp/equipmentInspectionApproval";
export default {
name: "DailyInspectionReport",
data() {
+ const d = new Date();
+ const today = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
+ const routeQuery = this.$route && this.$route.query || {};
+ const hasRouteQuery = !!(routeQuery.productionLine || (routeQuery.startDate && routeQuery.endDate));
return {
loading: false,
- dateRange: [this.getToday(), this.getToday()],
- productionLine: 2,
+ dateRange: (routeQuery.startDate && routeQuery.endDate)
+ ? [routeQuery.startDate, routeQuery.endDate]
+ : [today, today],
+ productionLine: routeQuery.productionLine ? Number(routeQuery.productionLine) : 2,
lineList: [],
partList: [],
records: [],
+ hasRouteQuery,
};
},
computed: {
@@ -253,11 +264,34 @@ export default {
this.loading = false;
}
},
+ handleSubmitForApproval() {
+ if (!this.dateRange || this.dateRange.length !== 2) {
+ this.$modal.msgWarning("请选择时间段");
+ return;
+ }
+ if (!this.productionLine) {
+ this.$modal.msgWarning("请选择产线");
+ return;
+ }
+ this.$modal.confirm('确认将该时间段"' + this.dateRange[0] + '至' + this.dateRange[1] + '"的巡检日报提交审批吗?').then(() => {
+ this.loading = true;
+ return addEquipmentInspectionApproval({
+ productionLine: this.productionLine,
+ insStartTime: this.dateRange[0] + ' 00:00:00',
+ insEndTime: this.dateRange[1] + ' 23:59:59',
+ });
+ }).then(() => {
+ this.$modal.msgSuccess("送检成功");
+ this.loading = false;
+ }).catch(() => {
+ this.loading = false;
+ });
+ },
async loadLineList() {
try {
const res = await listProductionLine({ pageSize: 999 });
if (res.rows) this.lineList = res.rows;
- if (this.lineList.length > 0) {
+ if (!this.hasRouteQuery && this.lineList.length > 0) {
const suanYa = this.lineList.find(l => l.lineName === '酸轧线');
this.productionLine = suanYa ? suanYa.lineId : this.lineList[0].lineId;
}
diff --git a/klp-ui/src/views/wms/report/template/action.vue b/klp-ui/src/views/wms/report/template/action.vue
index 7a3f2b45..6e75bb3f 100644
--- a/klp-ui/src/views/wms/report/template/action.vue
+++ b/klp-ui/src/views/wms/report/template/action.vue
@@ -654,7 +654,7 @@ export default {
const [lossRes, outRes] = await Promise.all([
listCoilWithIds({ ...this.queryParams, actionIds: lossActionIds.join(',') || '', startTime: '', endTime: '', selectType: 'raw_material' }),
- listCoilWithIds({ ...this.queryParams, coilIds: outIds.join(',') || '', byCreateTimeStart: this.queryParams.startTime, byCreateTimeEnd: this.queryParams.endTime, selectType: 'product' }),
+ listCoilWithIds({ ...this.queryParams, coilIds: outIds.join(',') || '', startTime: '', endTime: '', byCreateTimeStart: this.queryParams.startTime, byCreateTimeEnd: this.queryParams.endTime, selectType: 'product' }),
]);
this.lossList = lossRes.rows.map(item => {
@@ -692,7 +692,7 @@ export default {
const [lossRes, outRes] = await Promise.all([
listCoilWithIds({ ...this.queryParams, actionIds: lossActionIds.join(',') || '', startTime: '', endTime: '', selectType: 'raw_material' }),
- listCoilWithIds({ ...this.queryParams, coilIds: outIds.join(',') || '', startTime: '', endTime: '', selectType: 'product' }),
+ listCoilWithIds({ ...this.queryParams, coilIds: outIds.join(',') || '', startTime: '', endTime: '', byCreateTimeStart: this.queryParams.startTime, byCreateTimeEnd: this.queryParams.endTime, selectType: 'product' }),
]);
if (this.reportType === 'out') {
diff --git a/klp-ui/src/views/wms/todo/index.vue b/klp-ui/src/views/wms/todo/index.vue
index 7fc7818b..2ec45127 100644
--- a/klp-ui/src/views/wms/todo/index.vue
+++ b/klp-ui/src/views/wms/todo/index.vue
@@ -9,6 +9,9 @@
+
+
+
@@ -19,6 +22,7 @@
import TranferCoilTable from '@/views/wms/coil/views/base/tranfer.vue'
import InspectionTask from '@/views/mes/qc/inspection/task.vue'
import CertificateBook from '@/views/mes/qc/certificate/book.vue'
+ import EquipmentInspectionApproval from '@/views/mes/eqp/check/approval.vue'
export default {
name: 'TodoIndex',
@@ -26,6 +30,7 @@
TranferCoilTable,
InspectionTask,
CertificateBook,
+ EquipmentInspectionApproval,
},
data() {
return {
From 3a0f729669dfb5dbc37f2ed2e22958dbb35c908e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=A0=82=E7=B3=96?= <2178503051@qq.com>
Date: Fri, 29 May 2026 17:13:57 +0800
Subject: [PATCH 13/13] =?UTF-8?q?feat(attendanceCheck):=20=E6=96=B0?=
=?UTF-8?q?=E5=A2=9E=E5=91=98=E5=B7=A5=E8=80=83=E5=8B=A4=E8=AF=A6=E6=83=85?=
=?UTF-8?q?=E5=BC=B9=E7=AA=97=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
1. 为员工列表列添加详情按钮,点击可查看个人考勤详情
2. 新增个人考勤汇总卡片,展示考勤统计数据
3. 支持表格和日历两种明细视图,根据天数自动切换
4. 添加考勤日历视图样式与状态标识
5. 补充相关计算属性与工具方法支撑新功能
---
.../wms/hrm/attendance/attendanceCheck.vue | 320 +++++++++++++++++-
1 file changed, 317 insertions(+), 3 deletions(-)
diff --git a/klp-ui/src/views/wms/hrm/attendance/attendanceCheck.vue b/klp-ui/src/views/wms/hrm/attendance/attendanceCheck.vue
index bbd29626..4bb16ad9 100644
--- a/klp-ui/src/views/wms/hrm/attendance/attendanceCheck.vue
+++ b/klp-ui/src/views/wms/hrm/attendance/attendanceCheck.vue
@@ -24,7 +24,12 @@
-
+
+
+ {{ scope.row.employeeName }}
+
+
+
+
+
+
+
+ {{ currentPersonData.employeeName }}
+ {{ personSummary.normal }}天
+ {{ personSummary.abnormal }}天
+ {{ personSummary.absentHalf }}天
+ {{ personSummary.absentFull }}天
+ {{ personSummary.noRecord }}天
+ {{ personSummary.totalLateMinutes }}分钟
+ {{ personSummary.totalEarlyMinutes }}分钟
+ ¥{{ personSummary.totalDeduct }}
+ {{ dateList.length }}天
+
+
+
考勤明细({{ dateList.length }}天)
+
+
+
+
+
+
+
+ {{ getStatusText(scope.row.overallStatus) }}
+
+
+
+ {{ formatTime(scope.row.p1FirstCheck) }}
+
+
+ {{ formatTime(scope.row.p1LastCheck) }}
+
+
+
+ {{ scope.row.p1LateMinutes }}
+ 0
+
+
+
+
+ {{ scope.row.p1EarlyMinutes }}
+ 0
+
+
+
+ ¥{{ scope.row.p1Deduct || '0.00' }}
+
+
+
+
+
+
+
+
{{ month.label }}
+
+
+
+ {{ cell.day }}
+ {{ getStatusText(cell.status) }}
+
+
+
+
+ 正常
+ 迟到/早退
+ 半天旷工
+ 全天旷工
+ 无记录
+
+
+
+
+
@@ -515,7 +595,12 @@ export default {
selectedUserIds: [],
detailRecordsLoading: false,
- detailRecordsList: []
+ detailRecordsList: [],
+
+ personDialogVisible: false,
+ personDialogTitle: '',
+ currentPersonData: null,
+ personLoading: false,
};
},
computed: {
@@ -533,7 +618,88 @@ export default {
key: String(emp.infoId),
label: emp.name + '(' + emp.dept + ')'
}))
- }
+ },
+ personSummary() {
+ if (!this.currentPersonData) return { normal: 0, abnormal: 0, absentHalf: 0, absentFull: 0, noRecord: 0, totalLateMinutes: 0, totalEarlyMinutes: 0, totalDeduct: '0.00' };
+ let normal = 0, abnormal = 0, absentHalf = 0, absentFull = 0, noRecord = 0;
+ let totalLate = 0, totalEarly = 0, totalDeduct = 0;
+ this.dateList.forEach(date => {
+ const record = this.currentPersonData[date];
+ if (!record) {
+ noRecord++;
+ return;
+ }
+ const status = record.overallStatus;
+ if (status === 'normal') normal++;
+ else if (status === 'absent_half') absentHalf++;
+ else if (status === 'absent_full') absentFull++;
+ else if (status === 'abnormal' || status === 'late_warn' || status === 'late_one' || status === 'late_two' || status === 'early_warn' || status === 'early_one' || status === 'early_two' || status === 'missed') abnormal++;
+ totalLate += Number(record.p1LateMinutes || 0) + Number(record.p2LateMinutes || 0);
+ totalEarly += Number(record.p1EarlyMinutes || 0) + Number(record.p2EarlyMinutes || 0);
+ totalDeduct += Number(record.p1Deduct || 0) + Number(record.p2Deduct || 0) + Number(record.totalDeduct || 0);
+ });
+ return { normal, abnormal, absentHalf, absentFull, noRecord, totalLateMinutes: totalLate, totalEarlyMinutes: totalEarly, totalDeduct: totalDeduct.toFixed(2) };
+ },
+ personViewMode() {
+ const days = this.dateList.length;
+ if (days <= 14) return 'table';
+ if (days <= 62) return 'calendar';
+ return 'table';
+ },
+ personDetailTableData() {
+ if (!this.currentPersonData) return [];
+ return this.dateList.map(date => {
+ const record = this.currentPersonData[date];
+ if (!record) {
+ return { workDate: date, shiftName: '-', overallStatus: null, p1FirstCheck: null, p1LastCheck: null, p2FirstCheck: null, p2LastCheck: null, p1LateMinutes: 0, p1EarlyMinutes: 0, p1Deduct: 0, p2LateMinutes: 0, p2EarlyMinutes: 0, p2Deduct: 0, totalDeduct: 0, remark: '' };
+ }
+ return {
+ workDate: date,
+ ...record,
+ p1LateMinutes: record.p1LateMinutes || 0,
+ p1EarlyMinutes: record.p1EarlyMinutes || 0,
+ };
+ });
+ },
+ calendarWeekDays() {
+ return ['日', '一', '二', '三', '四', '五', '六'];
+ },
+ personCalendarMonths() {
+ if (!this.dateList || this.dateList.length === 0) return [];
+ const months = {};
+ this.dateList.forEach(date => {
+ const d = new Date(date);
+ const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
+ if (!months[key]) {
+ months[key] = { key, label: `${d.getFullYear()}年${d.getMonth() + 1}月`, dates: [] };
+ }
+ months[key].dates.push(date);
+ });
+ return Object.values(months).map(month => {
+ const firstDate = new Date(month.dates[0]);
+ const year = firstDate.getFullYear();
+ const monthNum = firstDate.getMonth();
+ const firstDay = new Date(year, monthNum, 1);
+ const lastDay = new Date(year, monthNum + 1, 0);
+ const startDayOfWeek = firstDay.getDay();
+ const totalDays = lastDay.getDate();
+ const cells = [];
+ for (let i = 0; i < startDayOfWeek; i++) {
+ cells.push({ day: '', date: null });
+ }
+ for (let day = 1; day <= totalDays; day++) {
+ const dateStr = `${year}-${String(monthNum + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
+ const record = this.currentPersonData ? this.currentPersonData[dateStr] : null;
+ cells.push({
+ day,
+ date: dateStr,
+ status: record ? record.overallStatus : null,
+ inRange: month.dates.includes(dateStr),
+ });
+ }
+ return { ...month, cells };
+ });
+ },
},
created() {
this.getAllEmployees().then(() => {
@@ -1049,6 +1215,30 @@ export default {
}).finally(() => {
this.loading = false
})
+ },
+ handlePersonDetail(row) {
+ this.currentPersonData = row
+ this.personDialogTitle = row.employeeName + ' - 考勤总览'
+ this.personDialogVisible = true
+ },
+ getCalendarCellClass(cell) {
+ if (!cell.date) return 'cal-empty'
+ if (!cell.status) return 'cal-no-record'
+ switch (cell.status) {
+ case 'normal': return 'cal-normal'
+ case 'absent_half': return 'cal-absent-half'
+ case 'absent_full': return 'cal-absent-full'
+ case 'abnormal':
+ case 'late_warn':
+ case 'late_one':
+ case 'late_two':
+ case 'early_warn':
+ case 'early_one':
+ case 'early_two':
+ case 'missed':
+ return 'cal-abnormal'
+ default: return 'cal-default'
+ }
}
}
};
@@ -1229,4 +1419,128 @@ export default {
margin-right: 10px;
white-space: nowrap;
}
+
+.early-info {
+ color: #f56c6c;
+}
+
+.calendar-view {
+ margin-top: 10px;
+}
+
+.calendar-month {
+ margin-bottom: 24px;
+}
+
+.calendar-month-title {
+ margin: 0 0 8px 0;
+ font-size: 14px;
+ color: #303133;
+}
+
+.calendar-grid {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 2px;
+ border: 1px solid #ebeef5;
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.calendar-header {
+ text-align: center;
+ padding: 6px 4px;
+ background: #f5f7fa;
+ font-size: 12px;
+ color: #909399;
+ font-weight: bold;
+}
+
+.calendar-cell {
+ min-height: 50px;
+ padding: 4px;
+ background: #fff;
+ text-align: center;
+ font-size: 12px;
+ border-right: 1px solid #ebeef5;
+ border-bottom: 1px solid #ebeef5;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.calendar-day {
+ font-weight: bold;
+ color: #303133;
+ font-size: 13px;
+}
+
+.calendar-status {
+ font-size: 10px;
+ margin-top: 2px;
+}
+
+.cal-empty {
+ background: #f9f9f9;
+ color: #c0c4cc;
+}
+
+.cal-no-record {
+ background: #fdf6ec;
+ color: #e6a23c;
+}
+
+.cal-normal {
+ background: #f0f9eb;
+ color: #67c23a;
+}
+
+.cal-abnormal {
+ background: #fef0f0;
+ color: #f56c6c;
+}
+
+.cal-absent-half {
+ background: #f4f4f5;
+ color: #909399;
+}
+
+.cal-absent-full {
+ background: #e9e9eb;
+ color: #606266;
+}
+
+.cal-default {
+ background: #f5f7fa;
+ color: #c0c4cc;
+}
+
+.calendar-legend {
+ display: flex;
+ gap: 16px;
+ margin-top: 12px;
+ flex-wrap: wrap;
+ font-size: 12px;
+}
+
+.legend-item {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ color: #606266;
+}
+
+.legend-dot {
+ display: inline-block;
+ width: 12px;
+ height: 12px;
+ border-radius: 2px;
+}
+
+.legend-dot.normal { background: #f0f9eb; border: 1px solid #67c23a; }
+.legend-dot.late { background: #fef0f0; border: 1px solid #f56c6c; }
+.legend-dot.absent-half { background: #f4f4f5; border: 1px solid #909399; }
+.legend-dot.absent-full { background: #e9e9eb; border: 1px solid #606266; }
+.legend-dot.no-record { background: #fdf6ec; border: 1px solid #e6a23c; }