|
@@ -121,6 +121,8 @@ import {
|
|
|
formatDate,
|
|
formatDate,
|
|
|
translateStatus,
|
|
translateStatus,
|
|
|
queryAttendanceRule,
|
|
queryAttendanceRule,
|
|
|
|
|
+ getShiftForDate,
|
|
|
|
|
+ isRestDay,
|
|
|
} from "@/api/attendance";
|
|
} from "@/api/attendance";
|
|
|
import { getLocationWithFallback } from "@/utils/location";
|
|
import { getLocationWithFallback } from "@/utils/location";
|
|
|
import { extractTime } from "@/utils/format";
|
|
import { extractTime } from "@/utils/format";
|
|
@@ -137,6 +139,10 @@ const dailyDetail = ref(null); // 管理端 daily 返回(带 actualIn/Out 时
|
|
|
const monthLoading = ref(false);
|
|
const monthLoading = ref(false);
|
|
|
// 班次信息:ruleName + 上下班打卡时间(缓存住,detail 里用)
|
|
// 班次信息:ruleName + 上下班打卡时间(缓存住,detail 里用)
|
|
|
const shiftInfo = reactive({ ruleName: '', startTime: '', endTime: '' });
|
|
const shiftInfo = reactive({ ruleName: '', startTime: '', endTime: '' });
|
|
|
|
|
+// 周班次表:7 项 ISO 周一→周日,每项含 isRestDay / shiftName / segments。
|
|
|
|
|
+// 用于日历格在月度接口还没回来时也能正确判断哪些天是休息日,
|
|
|
|
|
+// 也用于详情卡在 daily 接口没数据时也能展示班次名 + 时间范围。
|
|
|
|
|
+const weeklySchedule = ref([]);
|
|
|
// 考勤规则加载状态:'loading' / 'ok' / 'empty'(未配置) / 'disabled'(不可打卡) / 'error'(接口失败)
|
|
// 考勤规则加载状态:'loading' / 'ok' / 'empty'(未配置) / 'disabled'(不可打卡) / 'error'(接口失败)
|
|
|
const ruleStatus = ref('loading');
|
|
const ruleStatus = ref('loading');
|
|
|
const ruleBlockedReason = ref(''); // 不可打卡的具体原因:草稿 / 自然到期 / 停用
|
|
const ruleBlockedReason = ref(''); // 不可打卡的具体原因:草稿 / 自然到期 / 停用
|
|
@@ -159,9 +165,12 @@ const summary = computed(() => {
|
|
|
let attendanceDays = 0;
|
|
let attendanceDays = 0;
|
|
|
let lateCount = 0;
|
|
let lateCount = 0;
|
|
|
let abnormalCount = 0;
|
|
let abnormalCount = 0;
|
|
|
|
|
+ const schedule = weeklySchedule.value;
|
|
|
for (const item of list) {
|
|
for (const item of list) {
|
|
|
const status = item.attendanceStatusList
|
|
const status = item.attendanceStatusList
|
|
|
- if (status === "REST") continue;
|
|
|
|
|
|
|
+ // 休息日判定:API 标 REST 或 周班次表里是休息日
|
|
|
|
|
+ // 后者兜底,避免后端漏标(比如月底补算没及时回写)让工作日误算成"出勤"
|
|
|
|
|
+ if (status === "REST" || isRestDay(item.attendanceDate, schedule)) continue;
|
|
|
workDays++
|
|
workDays++
|
|
|
if (status === "NORMAL") attendanceDays++
|
|
if (status === "NORMAL") attendanceDays++
|
|
|
if ((item.lateMinutes || 0) > 0) lateCount++
|
|
if ((item.lateMinutes || 0) > 0) lateCount++
|
|
@@ -184,20 +193,24 @@ const grid = computed(() => {
|
|
|
const key = (item.attendanceDate || "").slice(0, 10);
|
|
const key = (item.attendanceDate || "").slice(0, 10);
|
|
|
if (key) byDate.set(key, item);
|
|
if (key) byDate.set(key, item);
|
|
|
}
|
|
}
|
|
|
|
|
+ const schedule = weeklySchedule.value;
|
|
|
const cells = [];
|
|
const cells = [];
|
|
|
for (let i = 0; i < firstDay; i++) cells.push({ blank: true });
|
|
for (let i = 0; i < firstDay; i++) cells.push({ blank: true });
|
|
|
for (let d = 1; d <= totalDays; d++) {
|
|
for (let d = 1; d <= totalDays; d++) {
|
|
|
const ds = `${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
|
|
const ds = `${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
|
|
|
const data = byDate.get(ds);
|
|
const data = byDate.get(ds);
|
|
|
|
|
+ // 从周班次表查当天的班次信息(用于月度接口未加载/未标 REST 时的兜底着色)
|
|
|
|
|
+ const scheduleInfo = getShiftForDate(ds, schedule);
|
|
|
cells.push({
|
|
cells.push({
|
|
|
date: ds,
|
|
date: ds,
|
|
|
day: d,
|
|
day: d,
|
|
|
data,
|
|
data,
|
|
|
|
|
+ scheduleInfo,
|
|
|
isToday: ds === todayStr,
|
|
isToday: ds === todayStr,
|
|
|
// 选中态跟原来 today 走同一套蓝色样式,这样点击其他日期时蓝色高亮会跟着移动,
|
|
// 选中态跟原来 today 走同一套蓝色样式,这样点击其他日期时蓝色高亮会跟着移动,
|
|
|
// 跟 record 卡片的数据切换形成视觉上的对应
|
|
// 跟 record 卡片的数据切换形成视觉上的对应
|
|
|
isSelected: ds === selectedDate.value,
|
|
isSelected: ds === selectedDate.value,
|
|
|
- dot: computeDot(data, ds === selectedDate.value),
|
|
|
|
|
|
|
+ dot: computeDot(data, ds === selectedDate.value, scheduleInfo),
|
|
|
dotCls: computeDotCls(data, ds === selectedDate.value),
|
|
dotCls: computeDotCls(data, ds === selectedDate.value),
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
@@ -205,7 +218,9 @@ const grid = computed(() => {
|
|
|
return cells;
|
|
return cells;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-function computeDot(data, isSelected) {
|
|
|
|
|
|
|
+function computeDot(data, isSelected, scheduleInfo) {
|
|
|
|
|
+ // 休息日且无打卡数据:不显示绿点(避免周末出现误导性的"已打卡"圆点)
|
|
|
|
|
+ if (scheduleInfo?.isRestDay && !data) return isSelected;
|
|
|
if (!data) return isSelected;
|
|
if (!data) return isSelected;
|
|
|
const status = data.attendanceStatusList;
|
|
const status = data.attendanceStatusList;
|
|
|
// 选中态:始终显示圆点(蓝底配白点)
|
|
// 选中态:始终显示圆点(蓝底配白点)
|
|
@@ -226,12 +241,15 @@ function computeDotCls(data, isSelected) {
|
|
|
function cellClass(cell) {
|
|
function cellClass(cell) {
|
|
|
if (cell.blank) return "blank";
|
|
if (cell.blank) return "blank";
|
|
|
const status = cell.data?.attendanceStatusList;
|
|
const status = cell.data?.attendanceStatusList;
|
|
|
|
|
+ // 休息日兜底:API 标 REST 或 周班次表里是休息日 都按休息日样式
|
|
|
|
|
+ // (后端漏标或接口延迟时,日历格依然能正确显示为灰色)
|
|
|
|
|
+ const isRest = status === "REST" || cell.scheduleInfo?.isRestDay;
|
|
|
const cls = [];
|
|
const cls = [];
|
|
|
// 选中态优先:被选中的日期复用原本 today 的蓝色背景(白字 + 白点)
|
|
// 选中态优先:被选中的日期复用原本 today 的蓝色背景(白字 + 白点)
|
|
|
if (cell.isSelected) cls.push("today");
|
|
if (cell.isSelected) cls.push("today");
|
|
|
// 今天的标记:仅在今天不是当前选中时显示,避免选中其他日期后"今天"完全消失
|
|
// 今天的标记:仅在今天不是当前选中时显示,避免选中其他日期后"今天"完全消失
|
|
|
if (cell.isToday && !cell.isSelected) cls.push("is-today-mark");
|
|
if (cell.isToday && !cell.isSelected) cls.push("is-today-mark");
|
|
|
- if (status === "REST") cls.push("rest");
|
|
|
|
|
|
|
+ if (isRest) cls.push("rest");
|
|
|
else if (status === "LEAVE") cls.push("leave");
|
|
else if (status === "LEAVE") cls.push("leave");
|
|
|
else if ((cell.data?.exceptionCount || 0) > 0) cls.push("abnormal");
|
|
else if ((cell.data?.exceptionCount || 0) > 0) cls.push("abnormal");
|
|
|
return cls.join(" ");
|
|
return cls.join(" ");
|
|
@@ -239,6 +257,10 @@ function cellClass(cell) {
|
|
|
|
|
|
|
|
const detail = computed(() => {
|
|
const detail = computed(() => {
|
|
|
const d = dailyDetail.value || {};
|
|
const d = dailyDetail.value || {};
|
|
|
|
|
+ // 选中日期的周班次(用于 daily 接口没返回数据时的兜底显示)
|
|
|
|
|
+ const scheduleInfo = getShiftForDate(selectedDate.value, weeklySchedule.value);
|
|
|
|
|
+ const isRest = Boolean(scheduleInfo?.isRestDay);
|
|
|
|
|
+
|
|
|
const inTime = d.actualIn || null;
|
|
const inTime = d.actualIn || null;
|
|
|
const outTime = d.actualOut || null;
|
|
const outTime = d.actualOut || null;
|
|
|
const inText = inTime ? extractTime(inTime) : "--:--";
|
|
const inText = inTime ? extractTime(inTime) : "--:--";
|
|
@@ -249,16 +271,32 @@ const detail = computed(() => {
|
|
|
const outLabel = outTime
|
|
const outLabel = outTime
|
|
|
? (d.earlyMinutes > 0 ? `早退 ${d.earlyMinutes} 分钟` : "正常")
|
|
? (d.earlyMinutes > 0 ? `早退 ${d.earlyMinutes} 分钟` : "正常")
|
|
|
: "未打卡";
|
|
: "未打卡";
|
|
|
- const start = d.scheduledStart || shiftInfo.startTime;
|
|
|
|
|
- const end = d.scheduledEnd || shiftInfo.endTime;
|
|
|
|
|
- const shiftText =
|
|
|
|
|
- ruleStatus.value === 'empty'
|
|
|
|
|
- ? "未配置考勤规则"
|
|
|
|
|
- : ruleStatus.value === 'disabled'
|
|
|
|
|
- ? `考勤规则${ruleBlockedReason.value || '不可用'}`
|
|
|
|
|
- : start && end
|
|
|
|
|
- ? `${shiftInfo.ruleName || "今日班次"} · ${extractTime(start)} - ${extractTime(end)}`
|
|
|
|
|
- : "休息日";
|
|
|
|
|
|
|
+ // 上下班时间:daily 返回 > 周班次表(当天非休息日时)> rule.shiftStartTime/End
|
|
|
|
|
+ let start = d.scheduledStart || '';
|
|
|
|
|
+ let end = d.scheduledEnd || '';
|
|
|
|
|
+ if ((!start || !end) && scheduleInfo && !scheduleInfo.isRestDay && scheduleInfo.segments.length) {
|
|
|
|
|
+ start = start || scheduleInfo.segments[0].start
|
|
|
|
|
+ end = end || scheduleInfo.segments[scheduleInfo.segments.length - 1].end
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!start) start = shiftInfo.startTime
|
|
|
|
|
+ if (!end) end = shiftInfo.endTime
|
|
|
|
|
+
|
|
|
|
|
+ let shiftText;
|
|
|
|
|
+ if (ruleStatus.value === 'empty') {
|
|
|
|
|
+ shiftText = "未配置考勤规则";
|
|
|
|
|
+ } else if (ruleStatus.value === 'disabled') {
|
|
|
|
|
+ shiftText = `考勤规则${ruleBlockedReason.value || '不可用'}`;
|
|
|
|
|
+ } else if (isRest && !d.actualIn && !d.actualOut) {
|
|
|
|
|
+ // 休息日且当天无打卡:显示「周X 休息日」(去掉冗余的"今日班次 · --:-- - --:--")
|
|
|
|
|
+ shiftText = `${scheduleInfo.weekdayLabel || ''} 休息日`.trim();
|
|
|
|
|
+ } else if (start && end) {
|
|
|
|
|
+ const name = (scheduleInfo && !scheduleInfo.isRestDay && scheduleInfo.shiftName)
|
|
|
|
|
+ || shiftInfo.ruleName
|
|
|
|
|
+ || "今日班次";
|
|
|
|
|
+ shiftText = `${name} · ${extractTime(start)} - ${extractTime(end)}`;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ shiftText = "休息日";
|
|
|
|
|
+ }
|
|
|
let workText = "";
|
|
let workText = "";
|
|
|
if (d.actualWorkMinutes || d.requiredMinutes) {
|
|
if (d.actualWorkMinutes || d.requiredMinutes) {
|
|
|
workText = `实际 ${d.actualWorkMinutes || 0} 分钟 / 应出勤 ${d.requiredMinutes || 0} 分钟`;
|
|
workText = `实际 ${d.actualWorkMinutes || 0} 分钟 / 应出勤 ${d.requiredMinutes || 0} 分钟`;
|
|
@@ -272,6 +310,10 @@ const detail = computed(() => {
|
|
|
const augmentedList = [...baseList]
|
|
const augmentedList = [...baseList]
|
|
|
if ((d.lateMinutes || 0) > 0 && !augmentedList.includes('LATE')) augmentedList.push('LATE')
|
|
if ((d.lateMinutes || 0) > 0 && !augmentedList.includes('LATE')) augmentedList.push('LATE')
|
|
|
if ((d.earlyMinutes || 0) > 0 && !augmentedList.includes('EARLY')) augmentedList.push('EARLY')
|
|
if ((d.earlyMinutes || 0) > 0 && !augmentedList.includes('EARLY')) augmentedList.push('EARLY')
|
|
|
|
|
+ // 休息日(按周班次表判定)补一个 REST,避免详情卡在无 daily 数据时漏掉"休息日"标签
|
|
|
|
|
+ if (isRest && !d.actualIn && !d.actualOut && !augmentedList.includes('REST')) {
|
|
|
|
|
+ augmentedList.push('REST')
|
|
|
|
|
+ }
|
|
|
const statusLabel = augmentedList.length > 0 ? translateStatus(augmentedList) : ''
|
|
const statusLabel = augmentedList.length > 0 ? translateStatus(augmentedList) : ''
|
|
|
return { shiftText, inText, outText, inLabel, outLabel, workText, statusLabel, actualOut: d.actualOut };
|
|
return { shiftText, inText, outText, inLabel, outLabel, workText, statusLabel, actualOut: d.actualOut };
|
|
|
});
|
|
});
|
|
@@ -355,6 +397,7 @@ async function loadShiftInfo() {
|
|
|
shiftInfo.ruleName = rule.ruleName || '';
|
|
shiftInfo.ruleName = rule.ruleName || '';
|
|
|
shiftInfo.startTime = rule.shiftStartTime || '';
|
|
shiftInfo.startTime = rule.shiftStartTime || '';
|
|
|
shiftInfo.endTime = rule.shiftEndTime || '';
|
|
shiftInfo.endTime = rule.shiftEndTime || '';
|
|
|
|
|
+ weeklySchedule.value = Array.isArray(rule.weeklySchedule) ? rule.weeklySchedule : [];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function refreshAll() {
|
|
async function refreshAll() {
|