|
|
@@ -540,12 +540,133 @@ const shiftCache = new Map() // shiftId -> { shift, fetchedAt
|
|
|
const RULE_TTL = 30 * 60 * 1000 // 30 分钟(规则可能调整)
|
|
|
const SHIFT_TTL = 24 * 60 * 60 * 1000 // 24 小时(班次很少变)
|
|
|
|
|
|
-// 把 shiftTimeSummary 拆成结构化的班次段数组。
|
|
|
+// ============ 周班次(按"周X:班次/休息"格式)============
|
|
|
// 后端实际下发形态:
|
|
|
-// "谢用规则班次1(09:00:00-18:00:00);谢用规则班次1(09:00:00-18:00:00);…"
|
|
|
+// "周一:谢用规则班次1(09:00:00-18:00:00);周二:谢用规则班次1(09:00:00-18:00:00);…
|
|
|
+// 周六:休息;周日:休息"
|
|
|
+// 段间分隔:; / ;
|
|
|
+// 段内分隔:"周X:" + 内容;内容为字面量"休息"表示休息日,否则视作
|
|
|
+// "班次名(HH:mm-HH:mm)" / "(HH:mm-HH:mm)" / "HH:mm-HH:mm"。
|
|
|
+
|
|
|
+// ISO 周一=1..周日=7;中文/英文/缩写都接住
|
|
|
+const WEEKDAY_KEYWORDS = {
|
|
|
+ '周一': 1, '周二': 2, '周三': 3, '周四': 4, '周五': 5, '周六': 6, '周日': 7,
|
|
|
+ '星期一': 1, '星期二': 2, '星期三': 3, '星期四': 4, '星期五': 5, '星期六': 6, '星期日': 7,
|
|
|
+ 'Monday': 1, 'Tuesday': 2, 'Wednesday': 3, 'Thursday': 4,
|
|
|
+ 'Friday': 5, 'Saturday': 6, 'Sunday': 7,
|
|
|
+ 'Mon': 1, 'Tue': 2, 'Wed': 3, 'Thu': 4, 'Fri': 5, 'Sat': 6, 'Sun': 7,
|
|
|
+}
|
|
|
+const WEEKDAY_LABELS = {
|
|
|
+ 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六', 7: '周日',
|
|
|
+}
|
|
|
+
|
|
|
+// 抓 "(前缀)(HH:mm-HH:mm)" / "(HH:mm-HH:mm)" / "HH:mm-HH:mm"
|
|
|
+// 前缀可缺省;时间部分接受 HH:mm 或 HH:mm:ss;连接符接受 - ~ 到 至
|
|
|
+const SHIFT_RANGE_RE = /(?:(.+?)\s*)?[((]\s*(\d{1,2}:\d{2}(?::\d{2})?)\s*[-~到至]\s*(\d{1,2}:\d{2}(?::\d{2})?)\s*[))]/
|
|
|
+const TIME_RANGE_ONLY_RE = /(\d{1,2}:\d{2}(?::\d{2})?)\s*[-~到至]\s*(\d{1,2}:\d{2}(?::\d{2})?)/
|
|
|
+
|
|
|
+// 把 shiftTimeSummary 解析成 7 项 ISO 周一→周日 数组。
|
|
|
+// 缺的日期默认填休息,避免下游下标错位。
|
|
|
+// 解析失败/空串 → 全休息的 7 项数组(不要被当成"无规则"误判)。
|
|
|
+export function parseShiftWeeklySummary(summary) {
|
|
|
+ const restStub = (i) => ({
|
|
|
+ weekday: i,
|
|
|
+ weekdayLabel: WEEKDAY_LABELS[i],
|
|
|
+ isRestDay: true,
|
|
|
+ shiftName: '',
|
|
|
+ segments: [],
|
|
|
+ })
|
|
|
+ if (!summary || typeof summary !== 'string') {
|
|
|
+ return [1, 2, 3, 4, 5, 6, 7].map(restStub)
|
|
|
+ }
|
|
|
+
|
|
|
+ const byWeekday = {}
|
|
|
+ for (const part of summary.split(/[;;]/).map((s) => s.trim()).filter(Boolean)) {
|
|
|
+ // "<周X>:<内容>",全/半角冒号都接
|
|
|
+ const m = part.match(/^([^::]+)[::]\s*(.*)$/)
|
|
|
+ if (!m) continue
|
|
|
+ const weekday = WEEKDAY_KEYWORDS[m[1].trim()]
|
|
|
+ if (!weekday) continue
|
|
|
+ const content = m[2].trim()
|
|
|
+
|
|
|
+ // 休息日:字面量 "休息" / "休" / "休息日"
|
|
|
+ if (/^休(息|息日)?$/.test(content)) {
|
|
|
+ byWeekday[weekday] = {
|
|
|
+ weekday,
|
|
|
+ weekdayLabel: WEEKDAY_LABELS[weekday],
|
|
|
+ isRestDay: true,
|
|
|
+ shiftName: '',
|
|
|
+ segments: [],
|
|
|
+ }
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ // 工作日:先抓 "前缀(time-time)";抓不到再尝试纯 time-time
|
|
|
+ const withName = content.match(SHIFT_RANGE_RE)
|
|
|
+ if (withName) {
|
|
|
+ byWeekday[weekday] = {
|
|
|
+ weekday,
|
|
|
+ weekdayLabel: WEEKDAY_LABELS[weekday],
|
|
|
+ isRestDay: false,
|
|
|
+ shiftName: (withName[1] || '').trim(),
|
|
|
+ segments: [{
|
|
|
+ start: withName[2].slice(0, 5),
|
|
|
+ end: withName[3].slice(0, 5),
|
|
|
+ raw: content,
|
|
|
+ }],
|
|
|
+ }
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ const bare = content.match(TIME_RANGE_ONLY_RE)
|
|
|
+ if (bare) {
|
|
|
+ byWeekday[weekday] = {
|
|
|
+ weekday,
|
|
|
+ weekdayLabel: WEEKDAY_LABELS[weekday],
|
|
|
+ isRestDay: false,
|
|
|
+ shiftName: '',
|
|
|
+ segments: [{
|
|
|
+ start: bare[1].slice(0, 5),
|
|
|
+ end: bare[2].slice(0, 5),
|
|
|
+ raw: content,
|
|
|
+ }],
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 完全无法识别的段就跳过,不污染其他日期
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = []
|
|
|
+ for (let i = 1; i <= 7; i++) result.push(byWeekday[i] || restStub(i))
|
|
|
+ return result
|
|
|
+}
|
|
|
+
|
|
|
+// JS getDay() 0=Sun..6=Sat → ISO 1=Mon..7=Sun
|
|
|
+function jsDayToIso(jsDay) {
|
|
|
+ return jsDay === 0 ? 7 : jsDay
|
|
|
+}
|
|
|
+
|
|
|
+// 给定日期 + 周班次数组,返回当天的班次信息(含 isRestDay)。
|
|
|
+// schedule 缺失/空 → null(让上游按"未配置"处理,而不是误报休息日)
|
|
|
+export function getShiftForDate(dateStr, schedule) {
|
|
|
+ if (!dateStr || !Array.isArray(schedule) || schedule.length === 0) return null
|
|
|
+ const d = new Date(dateStr)
|
|
|
+ if (Number.isNaN(d.getTime())) return null
|
|
|
+ return schedule[jsDayToIso(d.getDay()) - 1] || null
|
|
|
+}
|
|
|
+
|
|
|
+// 给定日期 + 周班次数组,是否为休息日。
|
|
|
+// 找不到 schedule → false(宁可选成工作日也别误报休息日)
|
|
|
+export function isRestDay(dateStr, schedule) {
|
|
|
+ const info = getShiftForDate(dateStr, schedule)
|
|
|
+ return info ? Boolean(info.isRestDay) : false
|
|
|
+}
|
|
|
+
|
|
|
+// 把 shiftTimeSummary 拆成结构化的班次段数组(仅时间区间)。
|
|
|
+// 后端实际下发形态:
|
|
|
+// "周一:谢用规则班次1(09:00:00-18:00:00);周二:谢用规则班次1(09:00:00-18:00:00);…"
|
|
|
// 段间用 ";" 或 ";" 分隔,每段形如 "前缀(HH:mm:ss-HH:mm:ss)"。
|
|
|
// 也兼容纯 "HH:mm-HH:mm" / "HH:mm - HH:mm"。
|
|
|
// 返回 [{ start, end, raw }, …];解析失败返回 []。
|
|
|
+// 注:rest 段(如"周六:休息")不含时间区间,自然不会出现在结果里。
|
|
|
function parseShiftSummary(summary) {
|
|
|
if (!summary || typeof summary !== 'string') return []
|
|
|
const segments = []
|
|
|
@@ -567,6 +688,7 @@ function parseShiftSummary(summary) {
|
|
|
}
|
|
|
|
|
|
// demo fallback:行政班 + GPS 打卡 + 中盈产业园(匹配 punch-rule 新 schema)
|
|
|
+// shiftTimeSummary 用周班次形态,与服务端下发一致
|
|
|
const DEMO_RULE = {
|
|
|
ruleId: 1,
|
|
|
ruleCode: 'ADMIN_DAY',
|
|
|
@@ -583,7 +705,8 @@ const DEMO_RULE = {
|
|
|
{ locationName: '中盈产业园 A座', radius: 200, outsidePolicy: 'ALLOW' },
|
|
|
],
|
|
|
wifiDisplayNames: [],
|
|
|
- shiftTimeSummary: '08:30 - 17:30',
|
|
|
+ shiftTimeSummary:
|
|
|
+ '周一:行政班(08:30-17:30);周二:行政班(08:30-17:30);周三:行政班(08:30-17:30);周四:行政班(08:30-17:30);周五:行政班(08:30-17:30);周六:休息;周日:休息',
|
|
|
message: '',
|
|
|
timezone: 'Asia/Shanghai',
|
|
|
version: 1,
|
|
|
@@ -630,12 +753,7 @@ const DEMO_SHIFT = {
|
|
|
export async function queryAttendanceRule() {
|
|
|
if (!isServerMode()) {
|
|
|
return {
|
|
|
- rule: {
|
|
|
- ...DEMO_RULE,
|
|
|
- shiftSegments: [{ start: '08:30', end: '17:30', raw: DEMO_RULE.shiftTimeSummary }],
|
|
|
- shiftStartTime: '08:30',
|
|
|
- shiftEndTime: '17:30',
|
|
|
- },
|
|
|
+ rule: normalize(DEMO_RULE),
|
|
|
status: 'ok',
|
|
|
loaded: true,
|
|
|
}
|
|
|
@@ -653,16 +771,32 @@ export async function queryAttendanceRule() {
|
|
|
}
|
|
|
|
|
|
// 把 shiftTimeSummary 拆成结构化字段,方便 UI 直接拿 startTime/endTime。
|
|
|
- // shiftSegments 是全部班次段(多段三班倒用得到),shiftStartTime/End 始终取第一段作"主班次"。
|
|
|
+ // - shiftSegments 全部时间区间(按"段"线性拉平,跨段三班倒用得到)
|
|
|
+ // - weeklySchedule ISO 周一→周日 7 项数组,每项含 isRestDay + segments
|
|
|
+ // - shiftStartTime/End 主班次的上下班(取第一个工作日的首/末段;取不到再退到 shiftSegments)
|
|
|
function normalize(rule) {
|
|
|
if (!rule) return null
|
|
|
const segments = parseShiftSummary(rule.shiftTimeSummary)
|
|
|
- const first = segments[0] || {}
|
|
|
+ const weeklySchedule = parseShiftWeeklySummary(rule.shiftTimeSummary)
|
|
|
+ const workDay = Array.isArray(weeklySchedule)
|
|
|
+ ? weeklySchedule.find((d) => !d.isRestDay && d.segments.length > 0)
|
|
|
+ : null
|
|
|
+ let shiftStartTime = ''
|
|
|
+ let shiftEndTime = ''
|
|
|
+ if (workDay) {
|
|
|
+ const segs = workDay.segments
|
|
|
+ shiftStartTime = segs[0].start
|
|
|
+ shiftEndTime = segs[segs.length - 1].end
|
|
|
+ } else if (segments.length) {
|
|
|
+ shiftStartTime = segments[0].start
|
|
|
+ shiftEndTime = segments[segments.length - 1].end
|
|
|
+ }
|
|
|
return {
|
|
|
...rule,
|
|
|
shiftSegments: segments,
|
|
|
- shiftStartTime: first.start || '',
|
|
|
- shiftEndTime: first.end || '',
|
|
|
+ weeklySchedule,
|
|
|
+ shiftStartTime,
|
|
|
+ shiftEndTime,
|
|
|
}
|
|
|
}
|
|
|
|