feat(mobile-oa-h5): port OA kit runtime and wire duration sync on oa-start

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-11 14:02:59 +08:00
1 parent b638c787b1
commit e83d925994
19 files changed
+1941 -14

No files matched your search

@@ -52,4 +52,5 @@
- [x] `GET /api/mini/menus` + SQL 种子 `docs/sql/mobile_menu_oa_start.sql` + 首页拉菜单(失败时 fallback) - [x] `GET /api/mini/menus` + SQL 种子 `docs/sql/mobile_menu_oa_start.sql` + 首页拉菜单(失败时 fallback)
- [ ] 测试库执行 SQL 并在角色中勾选「发起审批」 - [ ] 测试库执行 SQL 并在角色中勾选「发起审批」
- [ ] 真机:列表 → 填写 → 草稿/提交 全链路 - [ ] 真机:列表 → 填写 → 草稿/提交 全链路
- [ ] Task 8 套件运行时(请假/加班时长等)与 Web 对齐(分期) - [x] Task 8 套件运行时(请假/加班/出差/转正/离职/owner 默认值)已移植并接入 `/oa-start`(节假日走 `/cn-holiday/range`)
- [ ] 真机:含套件模板与 Web 对照回归
+8
View File
@@ -79,4 +79,12 @@ export function fetchStartOaInstance(data) {
return post('/web/admin/oa-approval/instances', data) return post('/web/admin/oa-approval/instances', data)
} }
export function fetchCurrentUser() {
return get('/web/auth/current-user')
}
export function fetchCnHolidayRange(start, end) {
return get('/cn-holiday/range', { start, end })
}
export { post, put } export { post, put }
+40
View File
@@ -0,0 +1,40 @@
/** 套件运行时 re-export(自 web-admin 移植) */
export {
applyLeaveKitDurationsToFormData,
ensureLeaveKitRuleProps,
LEAVE_MANUAL_PREFIX,
listLeaveKitBindings,
markLeaveDurationManual
} from './kit/oa-leave-kit-runtime.js'
export {
applyOvertimeKitDefaults,
applyOvertimeKitDurationsToFormData,
ensureOvertimeKitRuleProps,
listOvertimeKitBindings,
markOvertimeDurationManual,
OVERTIME_MANUAL_PREFIX
} from './kit/oa-overtime-kit-runtime.js'
export {
applyTripKitDaysToFormData,
listTripKits,
TRIP_MANUAL_PREFIX
} from './kit/oa-trip-kit-runtime.js'
export {
applyConfirmationKitDateLinkage,
applyConfirmationKitDefaults,
ensureConfirmationKitVisibility,
listConfirmationKitBindings,
listManualConfirmationDateFields,
markConfirmationDateManual,
CONFIRMATION_MANUAL_PREFIX
} from './kit/oa-confirmation-kit-runtime.js'
export { applyResignationKitDefaults } from './kit/oa-resignation-kit-runtime.js'
export {
applyOwnerFieldDefaults,
ensureOwnerFieldRuleProps
} from './kit/oa-owner-field-defaults.js'
@@ -0,0 +1,16 @@
function addMonthsYmd(hireYmd, months) {
const s = String(hireYmd || "").trim();
if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return null;
if (!Number.isInteger(months) || months < 0) return null;
const [y, m, d] = s.split("-").map((x) => Number(x));
if (!y || !m || !d) return null;
const total = y * 12 + (m - 1) + months;
const ny = Math.floor(total / 12);
const nm = total % 12 + 1;
const lastDay = new Date(ny, nm, 0).getDate();
const nd = Math.min(d, lastDay);
return `${ny}-${String(nm).padStart(2, "0")}-${String(nd).padStart(2, "0")}`;
}
export {
addMonthsYmd
};
@@ -0,0 +1,208 @@
let _uidSeq = 0;
function uniqueId() {
const num = 370 + ++_uidSeq;
return "F" + Math.random().toString(36).slice(3, 6) + Number(`${Date.now()}`).toString(36) + num.toString(36) + "r";
}
const CONFIRMATION_KIT_FIELD_DEFS = [
{ key: "hireDate", title: "\u5165\u804C\u65E5\u671F", showProp: "showHireDate" },
{ key: "probationMonths", title: "\u8BD5\u7528\u671F\uFF08\u6708\uFF09", showProp: "showProbationMonths" },
{ key: "confirmationDate", title: "\u8F6C\u6B63\u65E5\u671F", showProp: "showConfirmationDate" },
{ key: "post", title: "\u804C\u4F4D", showProp: "showPost" },
{ key: "jobLevel", title: "\u5C97\u4F4D\u804C\u7EA7", showProp: "showJobLevel" },
{ key: "name", title: "\u59D3\u540D", showProp: "showName" },
{ key: "email", title: "\u90AE\u7BB1", showProp: "showEmail" },
{ key: "dept", title: "\u90E8\u95E8", showProp: "showDept" },
{ key: "phone", title: "\u624B\u673A\u53F7", showProp: "showPhone" }
];
function buildConfirmationKitChildren() {
const hireDateField = uniqueId();
const probationMonthsField = uniqueId();
const confirmationDateField = uniqueId();
const postField = uniqueId();
const jobLevelField = uniqueId();
const nameField = uniqueId();
const emailField = uniqueId();
const deptField = uniqueId();
const phoneField = uniqueId();
return {
hireDateField,
probationMonthsField,
confirmationDateField,
postField,
jobLevelField,
nameField,
emailField,
deptField,
phoneField,
children: [
{
type: "datePicker",
field: hireDateField,
title: "\u5165\u804C\u65E5\u671F",
info: "",
$required: true,
props: {
_fc_kit_field: "hireDate",
type: "date",
format: "YYYY-MM-DD",
valueFormat: "YYYY-MM-DD",
placeholder: "\u8BF7\u9009\u62E9\u5165\u804C\u65E5\u671F"
}
},
{
type: "inputNumber",
field: probationMonthsField,
title: "\u8BD5\u7528\u671F\uFF08\u6708\uFF09",
info: "",
$required: true,
props: {
_fc_kit_field: "probationMonths",
min: 0,
precision: 0,
controlsPosition: "right"
}
},
{
type: "datePicker",
field: confirmationDateField,
title: "\u8F6C\u6B63\u65E5\u671F",
info: "\u53EF\u6839\u636E\u5165\u804C\u65E5\u671F\u4E0E\u8BD5\u7528\u671F\u81EA\u52A8\u5EFA\u8BAE\uFF1B\u624B\u6539\u540E\u4E0D\u518D\u81EA\u52A8\u8054\u52A8",
$required: false,
props: {
_fc_kit_field: "confirmationDate",
type: "date",
format: "YYYY-MM-DD",
valueFormat: "YYYY-MM-DD",
placeholder: "\u8BF7\u9009\u62E9\u8F6C\u6B63\u65E5\u671F"
}
},
{
type: "post",
field: postField,
title: "\u804C\u4F4D",
info: "",
$required: false,
props: {
_fc_kit_field: "post",
multiple: true
}
},
{
type: "input",
field: jobLevelField,
title: "\u5C97\u4F4D\u804C\u7EA7",
info: "",
$required: false,
props: {
_fc_kit_field: "jobLevel",
placeholder: "\u8BF7\u8F93\u5165\u5C97\u4F4D\u804C\u7EA7"
}
},
{
type: "input",
field: nameField,
title: "\u59D3\u540D",
info: "",
$required: true,
props: {
_fc_kit_field: "name",
placeholder: "\u8BF7\u8F93\u5165\u59D3\u540D"
}
},
{
type: "input",
field: emailField,
title: "\u90AE\u7BB1",
info: "",
$required: false,
props: {
_fc_kit_field: "email",
placeholder: "\u8BF7\u8F93\u5165\u90AE\u7BB1"
}
},
{
type: "dept",
field: deptField,
title: "\u90E8\u95E8",
info: "",
$required: true,
props: {
_fc_kit_field: "dept",
multiple: false
}
},
{
type: "input",
field: phoneField,
title: "\u624B\u673A\u53F7",
info: "",
$required: false,
props: {
_fc_kit_field: "phone",
placeholder: "\u8BF7\u8F93\u5165\u624B\u673A\u53F7"
}
}
]
};
}
const confirmationKitDragRule = {
menu: "kit",
icon: "icon-idcard",
label: "\u8F6C\u6B63",
name: "confirmationKit",
input: false,
drag: true,
dragBtn: true,
mask: false,
inside: true,
rule() {
const built = buildConfirmationKitChildren();
return {
type: "confirmationKit",
props: {
showHireDate: true,
showProbationMonths: true,
showConfirmationDate: true,
showPost: true,
showJobLevel: true,
showName: true,
showEmail: true,
showDept: true,
showPhone: true,
hireDateField: built.hireDateField,
probationMonthsField: built.probationMonthsField,
confirmationDateField: built.confirmationDateField,
postField: built.postField,
jobLevelField: built.jobLevelField,
nameField: built.nameField,
emailField: built.emailField,
deptField: built.deptField,
phoneField: built.phoneField
},
children: built.children
};
},
props() {
const switches = CONFIRMATION_KIT_FIELD_DEFS.map((d) => ({
type: "switch",
field: d.showProp,
title: `\u663E\u793A\uFF1A${d.title}`,
value: true,
props: { activeText: "\u663E\u793A", inactiveText: "\u9690\u85CF" }
}));
return [
...switches,
{
type: "input",
field: "formCreateInfo",
title: "\u63D0\u793A",
value: "\u9690\u85CF\u5B57\u6BB5\u4ECD\u4F1A\u5199\u5165\u9ED8\u8BA4\u503C\uFF1B\u5BA1\u6279\u901A\u8FC7\u540E\u53EF\u80FD\u56DE\u5199\u7528\u6237\u4E3B\u6570\u636E\u3002",
props: { disabled: true, type: "textarea", rows: 2 }
}
];
}
};
export {
CONFIRMATION_KIT_FIELD_DEFS,
confirmationKitDragRule
};
@@ -0,0 +1,235 @@
import { addMonthsYmd } from "./oa-confirmation-date.js";
import {
CONFIRMATION_KIT_FIELD_DEFS
} from "./oa-confirmation-kit-rule.js";
import { resolveCurrentOwnerDept } from "./oa-owner-field-defaults.js";
const CONFIRMATION_MANUAL_PREFIX = "__manual_";
function asRules(schema) {
if (Array.isArray(schema)) return schema.filter((r) => r && typeof r === "object");
if (schema && typeof schema === "object") {
const s = schema;
const rules = s.rule || s.rules || s.fields;
if (Array.isArray(rules)) return rules.filter((r) => r && typeof r === "object");
if (s.type || s.field) return [s];
}
return [];
}
function walk(rules, out) {
for (const r of rules) {
out.push(r);
const children = r.children;
if (Array.isArray(children)) {
walk(
children.filter((c) => c && typeof c === "object"),
out
);
}
const props = r.props;
if (props && Array.isArray(props.rule)) {
walk(
props.rule.filter((c) => c && typeof c === "object"),
out
);
}
}
}
function kitChildren(kit) {
const props = kit.props && typeof kit.props === "object" ? kit.props : {};
if (Array.isArray(kit.children)) return kit.children;
if (Array.isArray(props.rule)) return props.rule;
return [];
}
function resolveBinding(kit) {
const props = kit.props && typeof kit.props === "object" ? kit.props : {};
const fields = {};
const propFieldMap = {
hireDateField: "hireDate",
probationMonthsField: "probationMonths",
confirmationDateField: "confirmationDate",
postField: "post",
jobLevelField: "jobLevel",
nameField: "name",
emailField: "email",
deptField: "dept",
phoneField: "phone"
};
for (const [propKey, sem] of Object.entries(propFieldMap)) {
const f = props[propKey];
if (f) fields[sem] = String(f);
}
for (const c of kitChildren(kit)) {
if (!c?.field) continue;
const sem = String(c.props?._fc_kit_field || "");
if (CONFIRMATION_KIT_FIELD_DEFS.some((d) => d.key === sem) && !fields[sem]) {
fields[sem] = String(c.field);
}
}
const visible = {};
for (const d of CONFIRMATION_KIT_FIELD_DEFS) {
const v = props[d.showProp];
visible[d.key] = v === void 0 || v === null ? true : !!v;
}
return {
fields,
visible,
confirmationDateField: fields.confirmationDate,
hireDateField: fields.hireDate,
probationMonthsField: fields.probationMonths
};
}
function listConfirmationKitBindings(schema) {
const flat = [];
walk(asRules(schema), flat);
const out = [];
for (const r of flat) {
if (String(r.type || "") !== "confirmationKit") continue;
out.push(resolveBinding(r));
}
return out;
}
function isEmptyValue(raw) {
if (raw == null || raw === "") return true;
if (Array.isArray(raw)) return raw.length === 0;
if (typeof raw === "object") {
const o = raw;
return !String(o.id || "").trim();
}
return false;
}
function ensureConfirmationKitVisibility(rules) {
if (!Array.isArray(rules)) return [];
return rules.map((raw) => {
if (!raw || typeof raw !== "object") return raw;
const rule = { ...raw };
if (String(rule.type || "") === "confirmationKit") {
const binding = resolveBinding(rule);
const patchChildren = (list) => list.map((c) => {
if (!c || typeof c !== "object") return c;
const child = { ...c };
const sem = String(child.props?._fc_kit_field || "");
const show = sem && CONFIRMATION_KIT_FIELD_DEFS.some((d) => d.key === sem) ? binding.visible[sem] !== false : true;
child.hidden = !show;
if (!show) {
child.$required = false;
if (child.validate) child.validate = [];
}
if (Array.isArray(child.children)) {
child.children = patchChildren(child.children);
}
return child;
});
if (Array.isArray(rule.children)) {
rule.children = patchChildren(rule.children);
}
if (Array.isArray(rule.props?.rule)) {
rule.props = {
...rule.props,
rule: patchChildren(rule.props.rule)
};
}
}
if (Array.isArray(rule.children)) {
rule.children = ensureConfirmationKitVisibility(rule.children);
}
if (Array.isArray(rule.props?.rule)) {
rule.props = {
...rule.props,
rule: ensureConfirmationKitVisibility(rule.props.rule)
};
}
return rule;
});
}
async function applyConfirmationKitDefaults(schema, formData, user) {
const data = { ...formData };
const kits = listConfirmationKitBindings(schema);
if (!kits.length || !user) return data;
const name = String(user.realName || user.nickName || user.userName || "").trim();
const email = String(user.email || "").trim();
const phone = String(user.phone || user.phoneNo || "").trim();
const hireDate = String(user.hireDate || "").trim();
const probationRaw = user.probationMonths;
const probationMonths = probationRaw === null || probationRaw === void 0 || probationRaw === "" ? null : Number(probationRaw);
const dept = await resolveCurrentOwnerDept(user);
for (const kit of kits) {
const setIfEmpty = (sem, value) => {
const field = kit.fields[sem];
if (!field) return;
if (!isEmptyValue(data[field])) return;
if (value === null || value === void 0 || value === "") return;
data[field] = value;
};
setIfEmpty("hireDate", hireDate || null);
if (probationMonths !== null && Number.isFinite(probationMonths) && probationMonths >= 0) {
setIfEmpty("probationMonths", Math.trunc(probationMonths));
}
setIfEmpty("name", name || null);
setIfEmpty("email", email || null);
setIfEmpty("phone", phone || null);
if (dept) setIfEmpty("dept", dept);
const confField = kit.confirmationDateField;
const hireField = kit.hireDateField;
const probField = kit.probationMonthsField;
if (confField && isEmptyValue(data[confField]) && hireField && probField) {
const suggested = addMonthsYmd(
String(data[hireField] || ""),
Number(data[probField])
);
if (suggested) data[confField] = suggested;
}
}
return data;
}
function applyConfirmationKitDateLinkage(schema, formData) {
const data = { ...formData };
let changed = false;
for (const kit of listConfirmationKitBindings(schema)) {
const confField = kit.confirmationDateField;
const hireField = kit.hireDateField;
const probField = kit.probationMonthsField;
if (!confField || !hireField || !probField) continue;
if (data[CONFIRMATION_MANUAL_PREFIX + confField]) continue;
const suggested = addMonthsYmd(String(data[hireField] || ""), Number(data[probField]));
if (suggested == null) continue;
if (data[confField] !== suggested) {
data[confField] = suggested;
changed = true;
}
}
return { data, changed };
}
function markConfirmationDateManual(formData, confirmationDateField, manual = true) {
const data = { ...formData };
const key = CONFIRMATION_MANUAL_PREFIX + confirmationDateField;
if (manual) data[key] = true;
else delete data[key];
return data;
}
function clearConfirmationDateManualAndRelink(schema, formData, confirmationDateField) {
let data = markConfirmationDateManual(formData, confirmationDateField, false);
const linked = applyConfirmationKitDateLinkage(schema, data);
return linked.data;
}
function listManualConfirmationDateFields(schema, formData) {
return listConfirmationKitBindings(schema).map((k) => k.confirmationDateField).filter((f) => !!f && !!formData[CONFIRMATION_MANUAL_PREFIX + f]);
}
function isConfirmationDateField(schema, field) {
return listConfirmationKitBindings(schema).some((k) => k.confirmationDateField === field);
}
function findConfirmationKitByHireOrProbation(schema, field) {
return listConfirmationKitBindings(schema).find(
(k) => k.hireDateField === field || k.probationMonthsField === field
) || null;
}
export {
CONFIRMATION_MANUAL_PREFIX,
applyConfirmationKitDateLinkage,
applyConfirmationKitDefaults,
clearConfirmationDateManualAndRelink,
ensureConfirmationKitVisibility,
findConfirmationKitByHireOrProbation,
isConfirmationDateField,
listConfirmationKitBindings,
listManualConfirmationDateFields,
markConfirmationDateManual
};
+38
View File
@@ -0,0 +1,38 @@
function isDeptValueItem(v) {
if (!v || typeof v !== "object") return false;
const o = v;
return String(o.id || "").trim() !== "" && String(o.name || "").trim() !== "";
}
function normalizeDeptModelValue(raw, multiple) {
if (multiple) {
if (Array.isArray(raw)) {
return raw.filter(isDeptValueItem).map((x) => ({
id: String(x.id).trim(),
name: String(x.name).trim()
}));
}
if (isDeptValueItem(raw)) {
return [{ id: String(raw.id).trim(), name: String(raw.name).trim() }];
}
return [];
}
if (isDeptValueItem(raw)) {
return { id: String(raw.id).trim(), name: String(raw.name).trim() };
}
if (Array.isArray(raw) && raw.length === 1 && isDeptValueItem(raw[0])) {
return { id: String(raw[0].id).trim(), name: String(raw[0].name).trim() };
}
return null;
}
function formatDeptDisplay(raw) {
if (Array.isArray(raw)) {
return raw.filter(isDeptValueItem).map((x) => x.name).filter(Boolean).join("\u3001");
}
if (isDeptValueItem(raw)) return raw.name;
return "";
}
export {
formatDeptDisplay,
isDeptValueItem,
normalizeDeptModelValue
};
+71
View File
@@ -0,0 +1,71 @@
const DEFAULT_LEAVE_WORK_HOURS = {
morningStart: "08:30",
morningEnd: "12:00",
afternoonStart: "13:00",
afternoonEnd: "17:30"
};
const DT_RE = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})$/;
function parseHm(hm) {
const m = String(hm || "").trim().match(/^(\d{1,2}):(\d{2})$/);
if (!m) return null;
const h = Number(m[1]);
const min = Number(m[2]);
if (h < 0 || h > 23 || min < 0 || min > 59) return null;
return h * 60 + min;
}
function parseDt(s) {
const m = String(s || "").trim().match(DT_RE);
if (!m) return null;
const d = new Date(
Number(m[1]),
Number(m[2]) - 1,
Number(m[3]),
Number(m[4]),
Number(m[5]),
0,
0
);
return Number.isNaN(d.getTime()) ? null : d;
}
function overlapMinutesOnDay(day, rangeStart, rangeEnd, workHours) {
const y = day.getFullYear();
const mo = day.getMonth();
const da = day.getDate();
const day0 = new Date(y, mo, da, 0, 0, 0, 0).getTime();
const amA = parseHm(workHours.morningStart);
const amB = parseHm(workHours.morningEnd);
const pmA = parseHm(workHours.afternoonStart);
const pmB = parseHm(workHours.afternoonEnd);
if (amA == null || amB == null || pmA == null || pmB == null) return 0;
if (amA >= amB || pmA >= pmB) return 0;
const segments = [
[day0 + amA * 6e4, day0 + amB * 6e4],
[day0 + pmA * 6e4, day0 + pmB * 6e4]
];
const rs = rangeStart.getTime();
const re = rangeEnd.getTime();
let total = 0;
for (const [a, b] of segments) {
const lo = Math.max(rs, a);
const hi = Math.min(re, b);
if (hi > lo) total += (hi - lo) / 6e4;
}
return total;
}
function calcLeaveHours(startStr, endStr, workHours = DEFAULT_LEAVE_WORK_HOURS) {
const start = parseDt(startStr);
const end = parseDt(endStr);
if (!start || !end || end.getTime() <= start.getTime()) return null;
let minutes = 0;
const cursor = new Date(start.getFullYear(), start.getMonth(), start.getDate());
const last = new Date(end.getFullYear(), end.getMonth(), end.getDate());
while (cursor.getTime() <= last.getTime()) {
minutes += overlapMinutesOnDay(cursor, start, end, workHours);
cursor.setDate(cursor.getDate() + 1);
}
return Math.round(minutes / 60 * 10) / 10;
}
export {
DEFAULT_LEAVE_WORK_HOURS,
calcLeaveHours
};
@@ -0,0 +1,169 @@
import {
calcLeaveHours,
DEFAULT_LEAVE_WORK_HOURS
} from "./oa-leave-hours.js";
const LEAVE_MANUAL_PREFIX = "__manual_";
function asRules(schema) {
if (Array.isArray(schema)) return schema.filter((r) => r && typeof r === "object");
if (schema && typeof schema === "object") {
const s = schema;
const rules = s.rule || s.rules || s.fields;
if (Array.isArray(rules)) return rules.filter((r) => r && typeof r === "object");
if (s.type || s.field) return [s];
}
return [];
}
function walk(rules, out) {
for (const r of rules) {
out.push(r);
const children = r.children;
if (Array.isArray(children)) {
walk(
children.filter((c) => c && typeof c === "object"),
out
);
}
const props = r.props;
if (props && Array.isArray(props.rule)) {
walk(
props.rule.filter((c) => c && typeof c === "object"),
out
);
}
}
}
function resolveFields(kit) {
const props = kit.props && typeof kit.props === "object" ? kit.props : {};
let startField = props.startField;
let endField = props.endField;
let durationField = props.durationField;
const children = Array.isArray(kit.children) ? kit.children : Array.isArray(props.rule) ? props.rule : [];
if (!startField || !endField || !durationField) {
const dateFields = [];
let dur;
for (const c of children) {
if (!c?.field) continue;
const t = String(c.type || "");
if (t === "datePicker" || t === "date" || t === "timePicker" || t === "time") {
dateFields.push(String(c.field));
} else if ((t === "inputNumber" || t === "number") && !dur) {
dur = String(c.field);
}
}
startField = startField || dateFields[0];
endField = endField || dateFields[1];
durationField = durationField || dur;
}
if (!startField || !endField || !durationField) return null;
return {
startField,
endField,
durationField,
workHours: {
morningStart: String(props.morningStart || DEFAULT_LEAVE_WORK_HOURS.morningStart),
morningEnd: String(props.morningEnd || DEFAULT_LEAVE_WORK_HOURS.morningEnd),
afternoonStart: String(props.afternoonStart || DEFAULT_LEAVE_WORK_HOURS.afternoonStart),
afternoonEnd: String(props.afternoonEnd || DEFAULT_LEAVE_WORK_HOURS.afternoonEnd)
}
};
}
function listLeaveKitBindings(schema) {
const flat = [];
walk(asRules(schema), flat);
const out = [];
for (const r of flat) {
if (String(r.type || "") !== "leaveKit") continue;
const b = resolveFields(r);
if (b) out.push(b);
}
return out;
}
function applyLeaveKitDurationsToFormData(schema, formData) {
const data = { ...formData };
let changed = false;
for (const kit of listLeaveKitBindings(schema)) {
const manualKey = LEAVE_MANUAL_PREFIX + kit.durationField;
if (data[manualKey]) continue;
const hours = calcLeaveHours(
String(data[kit.startField] || ""),
String(data[kit.endField] || ""),
kit.workHours
);
if (hours == null) continue;
if (data[kit.durationField] !== hours) {
data[kit.durationField] = hours;
changed = true;
}
}
return { data, changed };
}
function markLeaveDurationManual(formData, durationField, manual = true) {
const data = { ...formData };
const key = LEAVE_MANUAL_PREFIX + durationField;
if (manual) data[key] = true;
else delete data[key];
return data;
}
function isLeaveDurationField(schema, field) {
return listLeaveKitBindings(schema).some((k) => k.durationField === field);
}
function findLeaveKitByStartOrEnd(schema, field) {
return listLeaveKitBindings(schema).find(
(k) => k.startField === field || k.endField === field
) || null;
}
function ensureLeaveKitRuleProps(rules) {
if (!Array.isArray(rules)) return [];
return rules.map((raw) => {
if (!raw || typeof raw !== "object") return raw;
const rule = { ...raw };
if (String(rule.type || "") === "leaveKit") {
const binding = resolveFields(rule);
const patchChildren = (list) => list.map((c) => {
if (!c || typeof c !== "object") return c;
const child = { ...c };
const t = String(child.type || "");
const isDuration = binding && child.field === binding.durationField || (t === "inputNumber" || t === "number") && String(child.title || "").includes("\u65F6\u957F");
if (isDuration) {
child.props = { ...child.props || {}, align: "left" };
const cls = String(child.class || child.className || "");
if (!cls.includes("oa-leave-duration")) {
child.class = (cls ? cls + " " : "") + "oa-leave-duration";
}
}
if (Array.isArray(child.children)) {
child.children = patchChildren(child.children);
}
return child;
});
if (Array.isArray(rule.children)) {
rule.children = patchChildren(rule.children);
}
if (Array.isArray(rule.props?.rule)) {
rule.props = {
...rule.props,
rule: patchChildren(rule.props.rule)
};
}
}
if (Array.isArray(rule.children)) {
rule.children = ensureLeaveKitRuleProps(rule.children);
}
if (Array.isArray(rule.props?.rule)) {
rule.props = {
...rule.props,
rule: ensureLeaveKitRuleProps(rule.props.rule)
};
}
return rule;
});
}
export {
LEAVE_MANUAL_PREFIX,
applyLeaveKitDurationsToFormData,
ensureLeaveKitRuleProps,
findLeaveKitByStartOrEnd,
isLeaveDurationField,
listLeaveKitBindings,
markLeaveDurationManual
};
@@ -0,0 +1,153 @@
const DEFAULT_OVERTIME_WINDOWS = {
workdayOvertimeStart: "17:30",
workdayOvertimeEnd: "08:30",
restBreakStart: "11:30",
restBreakEnd: "13:30"
};
const DT_RE = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})$/;
function parseHm(hm) {
const m = String(hm || "").trim().match(/^(\d{1,2}):(\d{2})$/);
if (!m) return null;
const h = Number(m[1]);
const min = Number(m[2]);
if (h < 0 || h > 23 || min < 0 || min > 59) return null;
return h * 60 + min;
}
function parseDt(s) {
const m = String(s || "").trim().match(DT_RE);
if (!m) return null;
const d = new Date(
Number(m[1]),
Number(m[2]) - 1,
Number(m[3]),
Number(m[4]),
Number(m[5]),
0,
0
);
return Number.isNaN(d.getTime()) ? null : d;
}
function formatYmd(d) {
const y = d.getFullYear();
const mo = String(d.getMonth() + 1).padStart(2, "0");
const da = String(d.getDate()).padStart(2, "0");
return `${y}-${mo}-${da}`;
}
function dayStartMs(d) {
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
}
function dayAtMinutes(d, minutes) {
return dayStartMs(d) + minutes * 6e4;
}
function mergeIntervals(intervals) {
if (intervals.length === 0) return [];
const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
const merged = [[sorted[0][0], sorted[0][1]]];
for (let i = 1; i < sorted.length; i++) {
const last = merged[merged.length - 1];
const [s, e] = sorted[i];
if (s <= last[1]) {
last[1] = Math.max(last[1], e);
} else {
merged.push([s, e]);
}
}
return merged;
}
function intersectInterval(a, b) {
const lo = Math.max(a[0], b[0]);
const hi = Math.min(a[1], b[1]);
return hi > lo ? [lo, hi] : null;
}
function subtractInterval(base, sub) {
const [bs, be] = base;
const [ss, se] = sub;
if (se <= bs || ss >= be) return [base];
const out = [];
if (ss > bs) out.push([bs, ss]);
if (se < be) out.push([se, be]);
return out;
}
function subtractIntervals(base, subs) {
let result = [base];
for (const sub of subs) {
const next = [];
for (const r of result) {
next.push(...subtractInterval(r, sub));
}
result = next;
}
return result;
}
function intervalMinutes(intervals) {
return intervals.reduce((sum, [a, b]) => sum + (b - a) / 6e4, 0);
}
function buildWorkdayWindows(anchorDay, windows) {
const startMin = parseHm(windows.workdayOvertimeStart);
const endMin = parseHm(windows.workdayOvertimeEnd);
if (startMin == null || endMin == null) return null;
const anchorStart = dayAtMinutes(anchorDay, startMin);
const nextDay = new Date(anchorDay);
nextDay.setDate(nextDay.getDate() + 1);
const anchorEnd = dayAtMinutes(nextDay, endMin);
if (anchorEnd <= anchorStart) return null;
return [anchorStart, anchorEnd];
}
function buildRestDaySegments(day, windows) {
const breakStart = parseHm(windows.restBreakStart);
const breakEnd = parseHm(windows.restBreakEnd);
if (breakStart == null || breakEnd == null) return [];
const day0 = dayStartMs(day);
const nextDay = new Date(day);
nextDay.setDate(nextDay.getDate() + 1);
const dayEnd = dayStartMs(nextDay);
const segments = [];
if (breakStart > 0) segments.push([day0, dayAtMinutes(day, breakStart)]);
if (breakEnd < 24 * 60) segments.push([dayAtMinutes(day, breakEnd), dayEnd]);
return segments.filter(([a, b]) => b > a);
}
function calcOvertimeHours(startStr, endStr, windows = DEFAULT_OVERTIME_WINDOWS, isRestDay) {
const start = parseDt(startStr);
const end = parseDt(endStr);
if (!start || !end || end.getTime() <= start.getTime()) return null;
const query = [start.getTime(), end.getTime()];
const startDay = new Date(start.getFullYear(), start.getMonth(), start.getDate());
const endDay = new Date(end.getFullYear(), end.getMonth(), end.getDate());
const workdayIntervals = [];
const anchor = new Date(startDay);
anchor.setDate(anchor.getDate() - 1);
const lastAnchor = new Date(endDay);
while (anchor.getTime() <= lastAnchor.getTime()) {
const ymd = formatYmd(anchor);
if (!isRestDay(ymd)) {
const win = buildWorkdayWindows(anchor, windows);
if (win) {
const hit = intersectInterval(win, query);
if (hit) workdayIntervals.push(hit);
}
}
anchor.setDate(anchor.getDate() + 1);
}
const workdayMerged = mergeIntervals(workdayIntervals);
const restIntervals = [];
const day = new Date(startDay);
while (day.getTime() <= endDay.getTime()) {
const ymd = formatYmd(day);
if (isRestDay(ymd)) {
for (const seg of buildRestDaySegments(day, windows)) {
const hit = intersectInterval(seg, query);
if (hit) {
const remaining = subtractIntervals(hit, workdayMerged);
restIntervals.push(...remaining);
}
}
}
day.setDate(day.getDate() + 1);
}
const totalMinutes = intervalMinutes(workdayMerged) + intervalMinutes(restIntervals);
return Math.round(totalMinutes / 60 * 10) / 10;
}
export {
DEFAULT_OVERTIME_WINDOWS,
calcOvertimeHours
};
@@ -0,0 +1,214 @@
import {
buildCurrentOwnerPerson
} from "./oa-owner-field-defaults.js";
import {
calcOvertimeHours,
DEFAULT_OVERTIME_WINDOWS
} from "./oa-overtime-hours.js";
const OVERTIME_MANUAL_PREFIX = "__manual_";
function asRules(schema) {
if (Array.isArray(schema)) return schema.filter((r) => r && typeof r === "object");
if (schema && typeof schema === "object") {
const s = schema;
const rules = s.rule || s.rules || s.fields;
if (Array.isArray(rules)) return rules.filter((r) => r && typeof r === "object");
if (s.type || s.field) return [s];
}
return [];
}
function walk(rules, out) {
for (const r of rules) {
out.push(r);
if (Array.isArray(r.children)) {
walk(r.children, out);
}
const props = r.props;
if (props && Array.isArray(props.rule)) walk(props.rule, out);
}
}
function resolveBinding(kit) {
const props = kit.props && typeof kit.props === "object" ? kit.props : {};
let personField = props.personField ? String(props.personField) : void 0;
let startField = props.startField ? String(props.startField) : void 0;
let endField = props.endField ? String(props.endField) : void 0;
let durationField = props.durationField ? String(props.durationField) : void 0;
const children = Array.isArray(kit.children) ? kit.children : Array.isArray(props.rule) ? props.rule : [];
for (const c of children) {
if (!c?.field) continue;
const sem = String(c.props?._fc_kit_field || "");
if (sem === "overtimePerson" && !personField) personField = String(c.field);
if (sem === "overtimeStart" && !startField) startField = String(c.field);
if (sem === "overtimeEnd" && !endField) endField = String(c.field);
if (sem === "overtimeDuration" && !durationField) durationField = String(c.field);
}
if (!startField || !endField || !durationField) {
const dateFields = [];
let dur;
for (const c of children) {
if (!c?.field) continue;
const t = String(c.type || "");
if (t === "datePicker" || t === "date" || t === "timePicker" || t === "time") {
dateFields.push(String(c.field));
} else if ((t === "inputNumber" || t === "number") && (String(c.title || "").includes("\u65F6\u957F") || String(c.props?._fc_kit_field || "") === "overtimeDuration")) {
dur = String(c.field);
}
}
startField = startField || dateFields[0];
endField = endField || dateFields[1];
durationField = durationField || dur;
}
if (!startField || !endField || !durationField) return null;
return {
personField,
startField,
endField,
durationField,
windows: {
workdayOvertimeStart: String(
props.workdayOvertimeStart || DEFAULT_OVERTIME_WINDOWS.workdayOvertimeStart
),
workdayOvertimeEnd: String(
props.workdayOvertimeEnd || DEFAULT_OVERTIME_WINDOWS.workdayOvertimeEnd
),
restBreakStart: String(props.restBreakStart || DEFAULT_OVERTIME_WINDOWS.restBreakStart),
restBreakEnd: String(props.restBreakEnd || DEFAULT_OVERTIME_WINDOWS.restBreakEnd)
}
};
}
function listOvertimeKitBindings(schema) {
const flat = [];
walk(asRules(schema), flat);
const out = [];
for (const r of flat) {
if (String(r.type || "") !== "overtimeKit") continue;
const b = resolveBinding(r);
if (b) out.push(b);
}
return out;
}
function isEmptyPersonValue(raw) {
if (raw == null || raw === "") return true;
if (Array.isArray(raw)) return raw.length === 0;
if (typeof raw === "object") {
const o = raw;
return !String(o.id || "").trim();
}
return false;
}
function applyOvertimeKitDefaults(schema, formData, user) {
const data = { ...formData };
const person = buildCurrentOwnerPerson(user);
if (!person) return data;
for (const kit of listOvertimeKitBindings(schema)) {
const f = kit.personField;
if (f && isEmptyPersonValue(data[f])) {
data[f] = [person];
}
}
return data;
}
function applyOvertimeKitDurationsToFormData(schema, formData, isRestDay) {
const data = { ...formData };
let changed = false;
for (const kit of listOvertimeKitBindings(schema)) {
const manualKey = OVERTIME_MANUAL_PREFIX + kit.durationField;
if (data[manualKey]) continue;
const hours = calcOvertimeHours(
String(data[kit.startField] || ""),
String(data[kit.endField] || ""),
kit.windows,
isRestDay
);
if (hours == null) continue;
if (data[kit.durationField] !== hours) {
data[kit.durationField] = hours;
changed = true;
}
}
return { data, changed };
}
function markOvertimeDurationManual(formData, durationField, manual = true) {
const data = { ...formData };
const key = OVERTIME_MANUAL_PREFIX + durationField;
if (manual) data[key] = true;
else delete data[key];
return data;
}
function isOvertimeDurationField(schema, field) {
return listOvertimeKitBindings(schema).some((k) => k.durationField === field);
}
function findOvertimeKitByStartOrEnd(schema, field) {
return listOvertimeKitBindings(schema).find(
(k) => k.startField === field || k.endField === field
) || null;
}
function ensureOvertimeKitRuleProps(rules) {
if (!Array.isArray(rules)) return [];
return rules.map((raw) => {
if (!raw || typeof raw !== "object") return raw;
const rule = { ...raw };
if (String(rule.type || "") === "overtimeKit") {
const binding = resolveBinding(rule);
const patchChildren = (list) => list.map((c) => {
if (!c || typeof c !== "object") return c;
const child = { ...c };
const t = String(child.type || "");
const isDuration = binding && child.field === binding.durationField || (t === "inputNumber" || t === "number") && String(child.title || "").includes("\u65F6\u957F");
if (isDuration) {
child.props = { ...child.props || {}, align: "left" };
const cls = String(child.class || child.className || "");
if (!cls.includes("oa-overtime-duration")) {
child.class = (cls ? cls + " " : "") + "oa-overtime-duration";
}
}
if (Array.isArray(child.children)) {
child.children = patchChildren(child.children);
}
return child;
});
if (Array.isArray(rule.children)) {
rule.children = patchChildren(rule.children);
}
if (Array.isArray(rule.props?.rule)) {
rule.props = {
...rule.props,
rule: patchChildren(rule.props.rule)
};
}
}
if (Array.isArray(rule.children)) {
rule.children = ensureOvertimeKitRuleProps(rule.children);
}
if (Array.isArray(rule.props?.rule)) {
rule.props = {
...rule.props,
rule: ensureOvertimeKitRuleProps(rule.props.rule)
};
}
return rule;
});
}
function hasOvertimeKit(schema) {
const flat = [];
const walk2 = (rules) => {
for (const r of rules) {
flat.push(r);
if (Array.isArray(r.children)) walk2(r.children);
const props = r.props;
if (props && Array.isArray(props.rule)) walk2(props.rule);
}
};
walk2(asRules(schema));
return flat.some((r) => String(r.type || "") === "overtimeKit");
}
export {
OVERTIME_MANUAL_PREFIX,
applyOvertimeKitDefaults,
applyOvertimeKitDurationsToFormData,
ensureOvertimeKitRuleProps,
findOvertimeKitByStartOrEnd,
hasOvertimeKit,
isOvertimeDurationField,
listOvertimeKitBindings,
markOvertimeDurationManual
};
@@ -0,0 +1,129 @@
import { fetchDeptTree } from "../../api/oa.js";
import { isDeptValueItem } from "./oa-dept-value.js";
import { isPersonValueItem } from "./oa-person-value.js";
import { buildPathLabelMapFromTreeRoots } from "./tree-path.js";
function walkRules(rules, out) {
for (const r of rules) {
if (!r || typeof r !== "object") continue;
const rule = r;
out.push(rule);
const children = rule.children || rule.props?.rule || [];
if (Array.isArray(children)) walkRules(children, out);
}
}
function flattenRules(schemaOrRules) {
if (Array.isArray(schemaOrRules)) {
const out = [];
walkRules(schemaOrRules, out);
return out;
}
if (schemaOrRules && typeof schemaOrRules === "object") {
const root = schemaOrRules;
const rules = root.rule || root.rules || [];
if (Array.isArray(rules)) {
const out = [];
walkRules(rules, out);
return out;
}
}
return [];
}
function isEmptyOwnerValue(raw) {
if (raw == null || raw === "") return true;
if (Array.isArray(raw)) return raw.length === 0;
if (typeof raw === "object") {
const o = raw;
return !String(o.id || "").trim();
}
return false;
}
function buildCurrentOwnerPerson(user) {
if (!user) return null;
const id = String(user.id || user.userId || "").trim();
if (!id) return null;
const name = String(user.realName || user.userName || user.username || "").trim() || id;
return { id, name };
}
function buildCurrentOwnerDeptSync(user) {
if (!user) return null;
const id = String(user.regionId || user.deptId || "").trim();
if (!id) return null;
const name = String(user.regionName || user.deptName || "").trim() || id;
return { id, name };
}
async function resolveCurrentOwnerDept(user) {
const base = buildCurrentOwnerDeptSync(user);
if (!base) return null;
try {
const tree = await fetchDeptTree() || [];
const map = buildPathLabelMapFromTreeRoots(tree, { nameKey: "deptName" });
const path = map.get(base.id);
if (path) return { id: base.id, name: path };
} catch {
}
return base;
}
async function applyOwnerFieldDefaults(schemaOrRules, formData, user) {
const rules = flattenRules(schemaOrRules);
const hasPerson = rules.some((r) => String(r.type || "") === "ownerPerson");
const hasDept = rules.some((r) => String(r.type || "") === "ownerDept");
if (!hasPerson && !hasDept) return { ...formData };
const person = hasPerson ? buildCurrentOwnerPerson(user) : null;
const dept = hasDept ? await resolveCurrentOwnerDept(user) : null;
const next = { ...formData };
for (const rule of rules) {
const type = String(rule.type || "");
const field = String(rule.field || rule.name || "").trim();
if (!field) continue;
const allowModify = rule.props?.allowModify === true;
if (type === "ownerPerson" && person) {
if (!allowModify || isEmptyOwnerValue(next[field])) {
next[field] = person;
}
}
if (type === "ownerDept" && dept) {
if (!allowModify || isEmptyOwnerValue(next[field])) {
next[field] = dept;
}
}
}
return next;
}
function ensureOwnerFieldRuleProps(rules) {
if (!Array.isArray(rules)) return [];
return rules.map((raw) => {
if (!raw || typeof raw !== "object") return raw;
const rule = { ...raw };
const type = String(rule.type || "");
if (type === "ownerPerson" || type === "ownerDept") {
const props = { ...rule.props || {} };
if (props.allowModify !== true) props.allowModify = false;
rule.props = props;
}
if (Array.isArray(rule.children)) {
rule.children = ensureOwnerFieldRuleProps(rule.children);
}
if (rule.props && Array.isArray(rule.props.rule)) {
rule.props = {
...rule.props,
rule: ensureOwnerFieldRuleProps(rule.props.rule)
};
}
return rule;
});
}
function isOwnerPersonValue(v) {
return isPersonValueItem(v);
}
function isOwnerDeptValue(v) {
return isDeptValueItem(v);
}
export {
applyOwnerFieldDefaults,
buildCurrentOwnerDeptSync,
buildCurrentOwnerPerson,
ensureOwnerFieldRuleProps,
isOwnerDeptValue,
isOwnerPersonValue,
resolveCurrentOwnerDept
};
@@ -0,0 +1,51 @@
function isPersonValueItem(v) {
if (!v || typeof v !== "object") return false;
const o = v;
return String(o.id || "").trim() !== "" && String(o.name || "").trim() !== "";
}
function normalizePersonModelValue(raw, multiple) {
if (multiple) {
if (Array.isArray(raw)) {
return raw.filter(isPersonValueItem).map((x) => ({
id: String(x.id).trim(),
name: String(x.name).trim()
}));
}
if (isPersonValueItem(raw)) {
return [{ id: String(raw.id).trim(), name: String(raw.name).trim() }];
}
return [];
}
if (isPersonValueItem(raw)) {
return { id: String(raw.id).trim(), name: String(raw.name).trim() };
}
if (Array.isArray(raw) && raw.length === 1 && isPersonValueItem(raw[0])) {
return { id: String(raw[0].id).trim(), name: String(raw[0].name).trim() };
}
return null;
}
function formatPersonDisplay(raw) {
if (Array.isArray(raw)) {
return raw.filter(isPersonValueItem).map((x) => x.name).filter(Boolean).join("\u3001");
}
if (isPersonValueItem(raw)) return raw.name;
return "";
}
function extractPersonIds(raw) {
const n = Array.isArray(raw) ? raw.filter(isPersonValueItem) : isPersonValueItem(raw) ? [raw] : [];
const out = [];
const seen = /* @__PURE__ */ new Set();
for (const x of n) {
const id = String(x.id).trim();
if (!id || seen.has(id)) continue;
seen.add(id);
out.push(id);
}
return out;
}
export {
extractPersonIds,
formatPersonDisplay,
isPersonValueItem,
normalizePersonModelValue
};
@@ -0,0 +1,56 @@
function asRules(schema) {
if (Array.isArray(schema)) return schema.filter((r) => r && typeof r === "object");
if (schema && typeof schema === "object") {
const s = schema;
const rules = s.rule || s.rules || s.fields;
if (Array.isArray(rules)) return rules.filter((r) => r && typeof r === "object");
if (s.type || s.field) return [s];
}
return [];
}
function walk(rules, out) {
for (const r of rules) {
out.push(r);
if (Array.isArray(r.children)) {
walk(r.children, out);
}
const props = r.props;
if (props && Array.isArray(props.rule)) walk(props.rule, out);
}
}
function resolveBinding(kit) {
const props = kit.props && typeof kit.props === "object" ? kit.props : {};
let hireDateField = props.hireDateField ? String(props.hireDateField) : void 0;
let lastWorkDayField = props.lastWorkDayField ? String(props.lastWorkDayField) : void 0;
const children = Array.isArray(kit.children) ? kit.children : Array.isArray(props.rule) ? props.rule : [];
for (const c of children) {
if (!c?.field) continue;
const sem = String(c.props?._fc_kit_field || "");
if (sem === "hireDate" && !hireDateField) hireDateField = String(c.field);
if (sem === "lastWorkDay" && !lastWorkDayField) lastWorkDayField = String(c.field);
}
return { hireDateField, lastWorkDayField };
}
function listResignationKitBindings(schema) {
const flat = [];
walk(asRules(schema), flat);
return flat.filter((r) => String(r.type || "") === "resignationKit").map(resolveBinding);
}
function isEmpty(raw) {
return raw == null || raw === "";
}
function applyResignationKitDefaults(schema, formData, user) {
const data = { ...formData };
if (!user) return data;
const hireDate = String(user.hireDate || "").trim();
if (!hireDate) return data;
for (const kit of listResignationKitBindings(schema)) {
const f = kit.hireDateField;
if (f && isEmpty(data[f])) data[f] = hireDate;
}
return data;
}
export {
applyResignationKitDefaults,
listResignationKitBindings
};
+62
View File
@@ -0,0 +1,62 @@
const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
function dayIndex(dateStr) {
const m = DATE_RE.exec(String(dateStr || "").trim());
if (!m) return null;
const y = Number(m[1]), mo = Number(m[2]), d = Number(m[3]);
const t = Date.UTC(y, mo - 1, d);
if (Number.isNaN(t)) return null;
return t / 864e5;
}
function startOffset(half) {
if (half === "\u4E0A\u5348") return 0;
if (half === "\u4E0B\u5348") return 0.5;
return null;
}
function endOffset(half) {
if (half === "\u4E0A\u5348") return 0.5;
if (half === "\u4E0B\u5348") return 1;
return null;
}
function calcTripLegDays(startDate, startHalf, endDate, endHalf) {
const ds = dayIndex(startDate);
const de = dayIndex(endDate);
const so = startOffset(String(startHalf || "").trim());
const eo = endOffset(String(endHalf || "").trim());
if (ds == null || de == null || so == null || eo == null) return null;
const duration = de + eo - (ds + so);
if (!(duration > 0)) return null;
return Math.round(duration * 2) / 2;
}
function sumTripLegDays(legs) {
let sum = 0;
for (const leg of legs || []) {
const n = Number(leg?.duration);
if (Number.isFinite(n) && n >= 0) sum += n;
}
return Math.round(sum * 2) / 2;
}
function isHalfDayMultiple(n) {
const v = Number(n);
if (!Number.isFinite(v) || v < 0) return false;
return Math.abs(v * 2 - Math.round(v * 2)) < 1e-9;
}
function emptyTripLeg() {
return {
transport: "",
tripType: "",
fromCity: "",
toCity: "",
startDate: "",
startHalf: "\u4E0A\u5348",
endDate: "",
endHalf: "\u4E0B\u5348",
duration: null,
durationManual: false
};
}
export {
calcTripLegDays,
emptyTripLeg,
isHalfDayMultiple,
sumTripLegDays
};
@@ -0,0 +1,105 @@
import { calcTripLegDays, sumTripLegDays } from "./oa-trip-days.js";
const TRIP_MANUAL_PREFIX = "__manual_";
function asRules(schema) {
if (Array.isArray(schema)) return schema.filter((r) => r && typeof r === "object");
if (schema && typeof schema === "object") {
const s = schema;
const rules = s.rule || s.rules || s.fields;
if (Array.isArray(rules)) return rules.filter((r) => r && typeof r === "object");
if (s.type || s.field) return [s];
}
return [];
}
function walk(rules, out) {
for (const r of rules) {
out.push(r);
const children = r.children;
if (Array.isArray(children)) {
walk(
children.filter((c) => c && typeof c === "object"),
out
);
}
const props = r.props;
if (props && Array.isArray(props.rule)) {
walk(
props.rule.filter((c) => c && typeof c === "object"),
out
);
}
}
}
function resolveTripKitFields(kit) {
const props = kit.props && typeof kit.props === "object" ? kit.props : {};
let listField = props.tripListField;
let daysField = props.tripDaysField;
if (listField && daysField) {
return { listField: String(listField), daysField: String(daysField) };
}
const children = Array.isArray(kit.children) ? kit.children : Array.isArray(props.rule) ? props.rule : [];
for (const c of children) {
if (!c?.field) continue;
const kitKey = String(c.props?._fc_kit_field || "");
if (kitKey === "tripList") listField = String(c.field);
else if (kitKey === "tripDays") daysField = String(c.field);
}
if (!listField || !daysField) return null;
return { listField, daysField };
}
function listTripKits(schema) {
const flat = [];
walk(asRules(schema), flat);
const out = [];
for (const r of flat) {
if (String(r.type || "") !== "tripKit") continue;
const ref = resolveTripKitFields(r);
if (ref) out.push(ref);
}
return out;
}
function applyTripKitDaysToFormData(schema, formData) {
const data = { ...formData };
let changed = false;
for (const kit of listTripKits(schema)) {
const legsRaw = data[kit.listField];
if (!Array.isArray(legsRaw)) continue;
const prevLegsJson = JSON.stringify(data[kit.listField]);
const legs = [];
for (const leg of legsRaw) {
if (!leg || typeof leg !== "object") {
legs.push(leg);
continue;
}
const row = { ...leg };
if (!row.durationManual) {
const days = calcTripLegDays(
String(row.startDate || ""),
String(row.startHalf || ""),
String(row.endDate || ""),
String(row.endHalf || "")
);
if (days != null && row.duration !== days) {
row.duration = days;
}
}
legs.push(row);
}
data[kit.listField] = legs;
if (JSON.stringify(legs) !== prevLegsJson) {
changed = true;
}
const manualKey = TRIP_MANUAL_PREFIX + kit.daysField;
if (data[manualKey]) continue;
const sum = sumTripLegDays(legs);
if (data[kit.daysField] !== sum) {
data[kit.daysField] = sum;
changed = true;
}
}
return { data, changed };
}
export {
TRIP_MANUAL_PREFIX,
applyTripKitDaysToFormData,
listTripKits
};
+75
View File
@@ -0,0 +1,75 @@
const DEFAULT_TREE_PATH_SEPARATOR = " / ";
function buildTreePathLabelFromFlat(nodeId, flatList, separatorOrOpts) {
const separator = typeof separatorOrOpts === "string" ? separatorOrOpts : separatorOrOpts?.separator ?? DEFAULT_TREE_PATH_SEPARATOR;
const nameKey = typeof separatorOrOpts === "string" ? "name" : separatorOrOpts?.nameKey ?? "name";
const cid = String(nodeId ?? "").trim();
if (!cid) return "";
const byId = /* @__PURE__ */ new Map();
for (const c of flatList || []) {
const id = String(c?.id ?? "").trim();
if (id) byId.set(id, c);
}
const names = [];
let cur = cid;
const seen = /* @__PURE__ */ new Set();
while (cur && byId.has(cur) && !seen.has(cur)) {
seen.add(cur);
const cat = byId.get(cur);
const nm = String(cat?.[nameKey] ?? "").trim();
if (nm) names.unshift(nm);
const pid = String(cat?.parentId ?? cat?.parent_id ?? "").trim();
cur = pid || null;
}
return names.length ? names.join(separator) : "";
}
function buildPathLabelMapFromTreeRoots(roots, options) {
const idKey = options?.idKey ?? "id";
const nameKey = options?.nameKey ?? "name";
const childrenKey = options?.childrenKey ?? "children";
const sep = options?.separator ?? DEFAULT_TREE_PATH_SEPARATOR;
const map = /* @__PURE__ */ new Map();
const walk = (nodes, pathNames) => {
for (const raw of nodes || []) {
const n = raw;
const id = n?.[idKey] != null ? String(n[idKey]).trim() : "";
const curName = String(n?.[nameKey] ?? "").trim();
const nextPath = curName ? [...pathNames, curName] : [...pathNames];
if (id) map.set(id, nextPath.join(sep));
const ch = n?.[childrenKey];
if (Array.isArray(ch) && ch.length) walk(ch, nextPath);
}
};
walk(roots || [], []);
return map;
}
function resolveTreePathDisplay(nodeId, flatList, fallbackName, separator = DEFAULT_TREE_PATH_SEPARATOR, emptyPlaceholder = "\u2014", nameKey) {
const path = buildTreePathLabelFromFlat(String(nodeId ?? "").trim() || void 0, flatList, {
separator,
nameKey
});
if (path) return path;
const fb = String(fallbackName ?? "").trim();
if (fb) return fb;
return emptyPlaceholder;
}
function findTreeSelectShortLabel(nodes, nodeId) {
const id = String(nodeId ?? "").trim();
if (!id) return "";
const walk = (arr) => {
for (const n of arr || []) {
if (!n) continue;
if (String(n.value ?? "") === id) return String(n.label ?? "").trim();
const hit = walk(n.children);
if (hit) return hit;
}
return "";
};
return walk(nodes);
}
export {
DEFAULT_TREE_PATH_SEPARATOR,
buildPathLabelMapFromTreeRoots,
buildTreePathLabelFromFlat,
findTreeSelectShortLabel,
resolveTreePathDisplay
};
+11 -1
View File
@@ -1,4 +1,10 @@
import { oaFormOption } from './form-option.js' import { oaFormOption } from './form-option.js'
import {
ensureLeaveKitRuleProps,
ensureOvertimeKitRuleProps,
ensureOwnerFieldRuleProps,
ensureConfirmationKitVisibility
} from './kit-runtime.js'
function walkRules(rules, fn) { function walkRules(rules, fn) {
const list = Array.isArray(rules) ? rules : [] const list = Array.isArray(rules) ? rules : []
@@ -28,7 +34,11 @@ export function prepareMobileFormSchema(formSchemaJson) {
: Array.isArray(schema) : Array.isArray(schema)
? schema ? schema
: [] : []
const rule = ensureOaUploadRuleProps(rawRule) const rule = ensureConfirmationKitVisibility(
ensureOwnerFieldRuleProps(
ensureOvertimeKitRuleProps(ensureLeaveKitRuleProps(ensureOaUploadRuleProps(rawRule)))
)
)
const option = { const option = {
...oaFormOption, ...oaFormOption,
...(schema.option || {}), ...(schema.option || {}),
+298 -12
View File
@@ -49,9 +49,30 @@ import {
fetchOaInstanceDraftDetail, fetchOaInstanceDraftDetail,
fetchSubmitOaInstanceDraft, fetchSubmitOaInstanceDraft,
fetchStartOaInstance, fetchStartOaInstance,
fetchUserList fetchUserList,
fetchCurrentUser,
fetchCnHolidayRange
} from '../api/oa.js' } from '../api/oa.js'
import { prepareMobileFormSchema } from '../oa/schema-prep.js' import { prepareMobileFormSchema } from '../oa/schema-prep.js'
import {
applyLeaveKitDurationsToFormData,
LEAVE_MANUAL_PREFIX,
listLeaveKitBindings,
markLeaveDurationManual,
applyOvertimeKitDefaults,
applyOvertimeKitDurationsToFormData,
listOvertimeKitBindings,
markOvertimeDurationManual,
OVERTIME_MANUAL_PREFIX,
applyTripKitDaysToFormData,
listTripKits,
TRIP_MANUAL_PREFIX,
applyConfirmationKitDateLinkage,
applyConfirmationKitDefaults,
listConfirmationKitBindings,
applyResignationKitDefaults,
applyOwnerFieldDefaults
} from '../oa/kit-runtime.js'
import OaFlowPreview from '../oa/components/OaFlowPreview.vue' import OaFlowPreview from '../oa/components/OaFlowPreview.vue'
const route = useRoute() const route = useRoute()
@@ -70,9 +91,19 @@ const flowResolved = ref(false)
const flowPendingMessage = ref('必填信息填写后,流程将自动显示') const flowPendingMessage = ref('必填信息填写后,流程将自动显示')
const initiatorSelectByNode = ref({}) const initiatorSelectByNode = ref({})
const userOptions = ref([]) const userOptions = ref([])
const currentUser = ref(null)
let previewTimer = null let previewTimer = null
let skipPreviewWatch = false let skipPreviewWatch = false
let leaveKitSyncing = false
let leaveKitPrevSnapshot = {}
let tripKitSyncing = false
let tripKitPrevSnapshot = {}
let confirmationKitSyncing = false
let confirmationKitPrevSnapshot = {}
let overtimeKitSyncing = false
let overtimeKitPrevSnapshot = {}
const overtimeRestMap = {}
function closeToMini(payload) { function closeToMini(payload) {
try { try {
@@ -87,6 +118,14 @@ function closeToMini(payload) {
showToast((payload && payload.message) || '已完成(非小程序环境)') showToast((payload && payload.message) || '已完成(非小程序环境)')
} }
async function loadCurrentUser() {
try {
currentUser.value = (await fetchCurrentUser()) || null
} catch {
currentUser.value = null
}
}
async function loadUsers() { async function loadUsers() {
try { try {
const pageSize = 100 const pageSize = 100
@@ -101,11 +140,13 @@ async function loadUsers() {
page += 1 page += 1
if (page > 50) break if (page > 50) break
} }
userOptions.value = raw.map((u) => ({ userOptions.value = raw
id: String(u.id || ''), .map((u) => ({
nickName: u.nickName || '', id: String(u.id || ''),
userNo: u.userNo || '' nickName: u.nickName || '',
})).filter((u) => u.id) userNo: u.userNo || ''
}))
.filter((u) => u.id)
} catch { } catch {
userOptions.value = [] userOptions.value = []
} }
@@ -155,7 +196,227 @@ function scheduleFlowPreview() {
}, 300) }, 300)
} }
async function loadByAppId(appId) { function syncLeaveKitDurations() {
if (leaveKitSyncing) return
if (!formRule.value.length) return
const schema = { rule: formRule.value }
const bindings = listLeaveKitBindings(schema)
if (!bindings.length) return
const curr = { ...formData.value }
for (const kit of bindings) {
const prevDur = leaveKitPrevSnapshot[kit.durationField]
const currDur = curr[kit.durationField]
const startChanged = leaveKitPrevSnapshot[kit.startField] !== curr[kit.startField]
const endChanged = leaveKitPrevSnapshot[kit.endField] !== curr[kit.endField]
const durChanged = prevDur !== currDur
if (durChanged && !startChanged && !endChanged && currDur !== undefined && currDur !== '') {
Object.assign(curr, markLeaveDurationManual(curr, kit.durationField, true))
}
}
const { data, changed } = applyLeaveKitDurationsToFormData(schema, curr)
leaveKitPrevSnapshot = { ...data }
const manualKeysChanged = bindings.some(
(k) =>
!!data[LEAVE_MANUAL_PREFIX + k.durationField] !==
!!formData.value[LEAVE_MANUAL_PREFIX + k.durationField]
)
const durationChanged = bindings.some(
(k) => data[k.durationField] !== formData.value[k.durationField]
)
if (changed || manualKeysChanged || durationChanged) {
leaveKitSyncing = true
formData.value = data
queueMicrotask(() => {
leaveKitSyncing = false
})
}
}
function syncTripKitDurations() {
if (tripKitSyncing) return
if (!formRule.value.length) return
const schema = { rule: formRule.value }
const kits = listTripKits(schema)
if (!kits.length) return
const curr = { ...formData.value }
for (const kit of kits) {
const prevDays = tripKitPrevSnapshot[kit.daysField]
const currDays = curr[kit.daysField]
const daysChanged = prevDays !== currDays
const listChanged =
JSON.stringify(tripKitPrevSnapshot[kit.listField]) !== JSON.stringify(curr[kit.listField])
if (daysChanged && !listChanged && currDays !== undefined && currDays !== '') {
curr[TRIP_MANUAL_PREFIX + kit.daysField] = true
}
}
const { data, changed } = applyTripKitDaysToFormData(schema, curr)
tripKitPrevSnapshot = { ...data }
const manualKeysChanged = kits.some(
(k) =>
!!data[TRIP_MANUAL_PREFIX + k.daysField] !==
!!formData.value[TRIP_MANUAL_PREFIX + k.daysField]
)
const daysFieldChanged = kits.some((k) => data[k.daysField] !== formData.value[k.daysField])
if (changed || manualKeysChanged || daysFieldChanged) {
tripKitSyncing = true
formData.value = data
queueMicrotask(() => {
tripKitSyncing = false
})
}
}
function syncConfirmationKitDates() {
if (confirmationKitSyncing) return
if (!formRule.value.length) return
const schema = { rule: formRule.value }
if (!listConfirmationKitBindings(schema).length) return
const curr = { ...formData.value }
const { data, changed } = applyConfirmationKitDateLinkage(schema, curr)
confirmationKitPrevSnapshot = { ...data }
if (changed) {
confirmationKitSyncing = true
formData.value = data
queueMicrotask(() => {
confirmationKitSyncing = false
})
}
}
async function ensureOvertimeHolidayRange(startYmd, endYmd) {
const start = String(startYmd || '').trim().slice(0, 10)
const end = String(endYmd || '').trim().slice(0, 10)
if (!start || !end) return
let lo = start
let hi = end
if (lo > hi) {
const t = lo
lo = hi
hi = t
}
const begin = new Date(lo + 'T00:00:00')
const finish = new Date(hi + 'T00:00:00')
if (Number.isNaN(begin.getTime()) || Number.isNaN(finish.getTime())) return
let allPresent = true
for (let d = new Date(begin); d <= finish; d.setDate(d.getDate() + 1)) {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const key = `${y}-${m}-${day}`
if (!(key in overtimeRestMap)) {
allPresent = false
break
}
}
if (allPresent) return
try {
const res = await fetchCnHolidayRange(lo, hi)
const days = res?.days || []
for (const day of days) {
const key = String(day.dayDate || '').slice(0, 10)
if (key) overtimeRestMap[key] = !!day.isRestDay
}
} catch {
/* 失败保留已有 map,计算回退周末规则 */
}
}
function isRestDayResolver(ymd) {
if (ymd in overtimeRestMap) return overtimeRestMap[ymd]
const d = new Date(ymd + 'T00:00:00')
const w = d.getDay()
return w === 0 || w === 6
}
async function syncOvertimeKitDurations() {
if (overtimeKitSyncing) return
if (!formRule.value.length) return
const schema = { rule: formRule.value }
const bindings = listOvertimeKitBindings(schema)
if (!bindings.length) return
overtimeKitSyncing = true
try {
let minYmd = ''
let maxYmd = ''
for (const kit of bindings) {
for (const field of [kit.startField, kit.endField]) {
const ymd = String(formData.value[field] || '')
.trim()
.slice(0, 10)
if (ymd.length < 10) continue
if (!minYmd || ymd < minYmd) minYmd = ymd
if (!maxYmd || ymd > maxYmd) maxYmd = ymd
}
}
if (minYmd && maxYmd) {
await ensureOvertimeHolidayRange(minYmd, maxYmd)
}
const curr = { ...formData.value }
for (const kit of bindings) {
const prevDur = overtimeKitPrevSnapshot[kit.durationField]
const currDur = curr[kit.durationField]
const startChanged = overtimeKitPrevSnapshot[kit.startField] !== curr[kit.startField]
const endChanged = overtimeKitPrevSnapshot[kit.endField] !== curr[kit.endField]
const durChanged = prevDur !== currDur
if (
durChanged &&
!startChanged &&
!endChanged &&
currDur !== undefined &&
currDur !== ''
) {
Object.assign(curr, markOvertimeDurationManual(curr, kit.durationField, true))
}
}
const { data, changed } = applyOvertimeKitDurationsToFormData(
schema,
curr,
isRestDayResolver
)
overtimeKitPrevSnapshot = { ...data }
const manualKeysChanged = bindings.some(
(k) =>
!!data[OVERTIME_MANUAL_PREFIX + k.durationField] !==
!!formData.value[OVERTIME_MANUAL_PREFIX + k.durationField]
)
const durationChanged = bindings.some(
(k) => data[k.durationField] !== formData.value[k.durationField]
)
if (changed || manualKeysChanged || durationChanged) {
formData.value = data
}
} finally {
queueMicrotask(() => {
overtimeKitSyncing = false
})
}
}
async function applyKitDefaults(baseData) {
let data = { ...(baseData || {}) }
const user = currentUser.value
data = await applyOwnerFieldDefaults(formRule.value, data, user)
data = await applyConfirmationKitDefaults({ rule: formRule.value }, data, user)
data = applyResignationKitDefaults({ rule: formRule.value }, data, user)
data = applyOvertimeKitDefaults({ rule: formRule.value }, data, user)
const linked = applyConfirmationKitDateLinkage({ rule: formRule.value }, data)
data = linked.data
confirmationKitPrevSnapshot = { ...data }
overtimeKitPrevSnapshot = { ...data }
leaveKitPrevSnapshot = { ...data }
tripKitPrevSnapshot = { ...data }
return data
}
async function loadByAppId(appId, { seedData = null, applyDefaults = true } = {}) {
const detail = await fetchOaTemplateDetail(appId) const detail = await fetchOaTemplateDetail(appId)
appMeta.value = { appMeta.value = {
id: appId, id: appId,
@@ -164,6 +425,16 @@ async function loadByAppId(appId) {
const prepared = prepareMobileFormSchema(detail?.formSchemaJson) const prepared = prepareMobileFormSchema(detail?.formSchemaJson)
formRule.value = prepared.rule formRule.value = prepared.rule
formOption.value = prepared.option formOption.value = prepared.option
if (applyDefaults) {
formData.value = await applyKitDefaults(seedData || {})
await syncOvertimeKitDurations()
} else if (seedData) {
formData.value = { ...seedData }
leaveKitPrevSnapshot = { ...formData.value }
tripKitPrevSnapshot = { ...formData.value }
confirmationKitPrevSnapshot = { ...formData.value }
overtimeKitPrevSnapshot = { ...formData.value }
}
await loadFlowPreview() await loadFlowPreview()
} }
@@ -171,6 +442,7 @@ async function bootstrap() {
detailLoading.value = true detailLoading.value = true
skipPreviewWatch = true skipPreviewWatch = true
try { try {
await loadCurrentUser()
const appId = String(route.query.appId || '').trim() const appId = String(route.query.appId || '').trim()
const draftId = String(route.query.draftId || '').trim() const draftId = String(route.query.draftId || '').trim()
if (draftId) { if (draftId) {
@@ -178,17 +450,18 @@ async function bootstrap() {
editingDraftId.value = draftId editingDraftId.value = draftId
const aid = String(d?.appId || '').trim() const aid = String(d?.appId || '').trim()
if (!aid) throw new Error('草稿缺少 appId') if (!aid) throw new Error('草稿缺少 appId')
formData.value = d.formData && typeof d.formData === 'object' ? { ...d.formData } : {}
initiatorSelectByNode.value = initiatorSelectByNode.value =
d.initiatorSelectByNode && typeof d.initiatorSelectByNode === 'object' d.initiatorSelectByNode && typeof d.initiatorSelectByNode === 'object'
? { ...d.initiatorSelectByNode } ? { ...d.initiatorSelectByNode }
: {} : {}
await loadByAppId(aid) const seed =
d.formData && typeof d.formData === 'object' ? { ...d.formData } : {}
// 草稿:保留已填值,仅补空的 owner 等默认
await loadByAppId(aid, { seedData: seed, applyDefaults: true })
} else if (appId) { } else if (appId) {
formData.value = {}
initiatorSelectByNode.value = {} initiatorSelectByNode.value = {}
editingDraftId.value = '' editingDraftId.value = ''
await loadByAppId(appId) await loadByAppId(appId, { seedData: {}, applyDefaults: true })
} else { } else {
showToast('缺少 appId') showToast('缺少 appId')
} }
@@ -288,7 +561,20 @@ async function submit() {
} }
watch( watch(
[formData, initiatorSelectByNode], formData,
() => {
if (skipPreviewWatch) return
syncLeaveKitDurations()
syncTripKitDurations()
syncConfirmationKitDates()
void syncOvertimeKitDurations()
scheduleFlowPreview()
},
{ deep: true }
)
watch(
initiatorSelectByNode,
() => { () => {
if (skipPreviewWatch) return if (skipPreviewWatch) return
scheduleFlowPreview() scheduleFlowPreview()