From e83d9259947db256767e340f503133754e4a4f24 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Tue, 11 Aug 2026 14:02:59 +0800 Subject: [PATCH] feat(mobile-oa-h5): port OA kit runtime and wire duration sync on oa-start Co-authored-by: Cursor --- ...6-08-11-mobile-oa-form-sink-spike-notes.md | 3 +- mobile-oa-h5/src/api/oa.js | 8 + mobile-oa-h5/src/oa/kit-runtime.js | 40 +++ .../src/oa/kit/oa-confirmation-date.js | 16 + .../src/oa/kit/oa-confirmation-kit-rule.js | 208 ++++++++++++ .../src/oa/kit/oa-confirmation-kit-runtime.js | 235 +++++++++++++ mobile-oa-h5/src/oa/kit/oa-dept-value.js | 38 +++ mobile-oa-h5/src/oa/kit/oa-leave-hours.js | 71 ++++ .../src/oa/kit/oa-leave-kit-runtime.js | 169 ++++++++++ mobile-oa-h5/src/oa/kit/oa-overtime-hours.js | 153 +++++++++ .../src/oa/kit/oa-overtime-kit-runtime.js | 214 ++++++++++++ .../src/oa/kit/oa-owner-field-defaults.js | 129 ++++++++ mobile-oa-h5/src/oa/kit/oa-person-value.js | 51 +++ .../src/oa/kit/oa-resignation-kit-runtime.js | 56 ++++ mobile-oa-h5/src/oa/kit/oa-trip-days.js | 62 ++++ .../src/oa/kit/oa-trip-kit-runtime.js | 105 ++++++ mobile-oa-h5/src/oa/kit/tree-path.js | 75 +++++ mobile-oa-h5/src/oa/schema-prep.js | 12 +- mobile-oa-h5/src/views/OaStart.vue | 310 +++++++++++++++++- 19 files changed, 1941 insertions(+), 14 deletions(-) create mode 100644 mobile-oa-h5/src/oa/kit-runtime.js create mode 100644 mobile-oa-h5/src/oa/kit/oa-confirmation-date.js create mode 100644 mobile-oa-h5/src/oa/kit/oa-confirmation-kit-rule.js create mode 100644 mobile-oa-h5/src/oa/kit/oa-confirmation-kit-runtime.js create mode 100644 mobile-oa-h5/src/oa/kit/oa-dept-value.js create mode 100644 mobile-oa-h5/src/oa/kit/oa-leave-hours.js create mode 100644 mobile-oa-h5/src/oa/kit/oa-leave-kit-runtime.js create mode 100644 mobile-oa-h5/src/oa/kit/oa-overtime-hours.js create mode 100644 mobile-oa-h5/src/oa/kit/oa-overtime-kit-runtime.js create mode 100644 mobile-oa-h5/src/oa/kit/oa-owner-field-defaults.js create mode 100644 mobile-oa-h5/src/oa/kit/oa-person-value.js create mode 100644 mobile-oa-h5/src/oa/kit/oa-resignation-kit-runtime.js create mode 100644 mobile-oa-h5/src/oa/kit/oa-trip-days.js create mode 100644 mobile-oa-h5/src/oa/kit/oa-trip-kit-runtime.js create mode 100644 mobile-oa-h5/src/oa/kit/tree-path.js diff --git a/docs/superpowers/specs/2026-08-11-mobile-oa-form-sink-spike-notes.md b/docs/superpowers/specs/2026-08-11-mobile-oa-form-sink-spike-notes.md index d9a2b736..ec116772 100644 --- a/docs/superpowers/specs/2026-08-11-mobile-oa-form-sink-spike-notes.md +++ b/docs/superpowers/specs/2026-08-11-mobile-oa-form-sink-spike-notes.md @@ -52,4 +52,5 @@ - [x] `GET /api/mini/menus` + SQL 种子 `docs/sql/mobile_menu_oa_start.sql` + 首页拉菜单(失败时 fallback) - [ ] 测试库执行 SQL 并在角色中勾选「发起审批」 - [ ] 真机:列表 → 填写 → 草稿/提交 全链路 -- [ ] Task 8 套件运行时(请假/加班时长等)与 Web 对齐(分期) +- [x] Task 8 套件运行时(请假/加班/出差/转正/离职/owner 默认值)已移植并接入 `/oa-start`(节假日走 `/cn-holiday/range`) +- [ ] 真机:含套件模板与 Web 对照回归 diff --git a/mobile-oa-h5/src/api/oa.js b/mobile-oa-h5/src/api/oa.js index 24a97ab6..e4af0805 100644 --- a/mobile-oa-h5/src/api/oa.js +++ b/mobile-oa-h5/src/api/oa.js @@ -79,4 +79,12 @@ export function fetchStartOaInstance(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 } diff --git a/mobile-oa-h5/src/oa/kit-runtime.js b/mobile-oa-h5/src/oa/kit-runtime.js new file mode 100644 index 00000000..edfc0589 --- /dev/null +++ b/mobile-oa-h5/src/oa/kit-runtime.js @@ -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' diff --git a/mobile-oa-h5/src/oa/kit/oa-confirmation-date.js b/mobile-oa-h5/src/oa/kit/oa-confirmation-date.js new file mode 100644 index 00000000..5a354ab2 --- /dev/null +++ b/mobile-oa-h5/src/oa/kit/oa-confirmation-date.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 +}; diff --git a/mobile-oa-h5/src/oa/kit/oa-confirmation-kit-rule.js b/mobile-oa-h5/src/oa/kit/oa-confirmation-kit-rule.js new file mode 100644 index 00000000..584129ce --- /dev/null +++ b/mobile-oa-h5/src/oa/kit/oa-confirmation-kit-rule.js @@ -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 +}; diff --git a/mobile-oa-h5/src/oa/kit/oa-confirmation-kit-runtime.js b/mobile-oa-h5/src/oa/kit/oa-confirmation-kit-runtime.js new file mode 100644 index 00000000..6e281484 --- /dev/null +++ b/mobile-oa-h5/src/oa/kit/oa-confirmation-kit-runtime.js @@ -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 +}; diff --git a/mobile-oa-h5/src/oa/kit/oa-dept-value.js b/mobile-oa-h5/src/oa/kit/oa-dept-value.js new file mode 100644 index 00000000..b70a6acd --- /dev/null +++ b/mobile-oa-h5/src/oa/kit/oa-dept-value.js @@ -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 +}; diff --git a/mobile-oa-h5/src/oa/kit/oa-leave-hours.js b/mobile-oa-h5/src/oa/kit/oa-leave-hours.js new file mode 100644 index 00000000..beeee4c1 --- /dev/null +++ b/mobile-oa-h5/src/oa/kit/oa-leave-hours.js @@ -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 +}; diff --git a/mobile-oa-h5/src/oa/kit/oa-leave-kit-runtime.js b/mobile-oa-h5/src/oa/kit/oa-leave-kit-runtime.js new file mode 100644 index 00000000..12f0438b --- /dev/null +++ b/mobile-oa-h5/src/oa/kit/oa-leave-kit-runtime.js @@ -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 +}; diff --git a/mobile-oa-h5/src/oa/kit/oa-overtime-hours.js b/mobile-oa-h5/src/oa/kit/oa-overtime-hours.js new file mode 100644 index 00000000..7395efa8 --- /dev/null +++ b/mobile-oa-h5/src/oa/kit/oa-overtime-hours.js @@ -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 +}; diff --git a/mobile-oa-h5/src/oa/kit/oa-overtime-kit-runtime.js b/mobile-oa-h5/src/oa/kit/oa-overtime-kit-runtime.js new file mode 100644 index 00000000..0ead86c5 --- /dev/null +++ b/mobile-oa-h5/src/oa/kit/oa-overtime-kit-runtime.js @@ -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 +}; diff --git a/mobile-oa-h5/src/oa/kit/oa-owner-field-defaults.js b/mobile-oa-h5/src/oa/kit/oa-owner-field-defaults.js new file mode 100644 index 00000000..c12e7a3b --- /dev/null +++ b/mobile-oa-h5/src/oa/kit/oa-owner-field-defaults.js @@ -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 +}; diff --git a/mobile-oa-h5/src/oa/kit/oa-person-value.js b/mobile-oa-h5/src/oa/kit/oa-person-value.js new file mode 100644 index 00000000..296cfccb --- /dev/null +++ b/mobile-oa-h5/src/oa/kit/oa-person-value.js @@ -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 +}; diff --git a/mobile-oa-h5/src/oa/kit/oa-resignation-kit-runtime.js b/mobile-oa-h5/src/oa/kit/oa-resignation-kit-runtime.js new file mode 100644 index 00000000..0974a02c --- /dev/null +++ b/mobile-oa-h5/src/oa/kit/oa-resignation-kit-runtime.js @@ -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 +}; diff --git a/mobile-oa-h5/src/oa/kit/oa-trip-days.js b/mobile-oa-h5/src/oa/kit/oa-trip-days.js new file mode 100644 index 00000000..fdd3171e --- /dev/null +++ b/mobile-oa-h5/src/oa/kit/oa-trip-days.js @@ -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 +}; diff --git a/mobile-oa-h5/src/oa/kit/oa-trip-kit-runtime.js b/mobile-oa-h5/src/oa/kit/oa-trip-kit-runtime.js new file mode 100644 index 00000000..56071bf1 --- /dev/null +++ b/mobile-oa-h5/src/oa/kit/oa-trip-kit-runtime.js @@ -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 +}; diff --git a/mobile-oa-h5/src/oa/kit/tree-path.js b/mobile-oa-h5/src/oa/kit/tree-path.js new file mode 100644 index 00000000..c75250ee --- /dev/null +++ b/mobile-oa-h5/src/oa/kit/tree-path.js @@ -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 +}; diff --git a/mobile-oa-h5/src/oa/schema-prep.js b/mobile-oa-h5/src/oa/schema-prep.js index 44464865..0f7b4c1a 100644 --- a/mobile-oa-h5/src/oa/schema-prep.js +++ b/mobile-oa-h5/src/oa/schema-prep.js @@ -1,4 +1,10 @@ import { oaFormOption } from './form-option.js' +import { + ensureLeaveKitRuleProps, + ensureOvertimeKitRuleProps, + ensureOwnerFieldRuleProps, + ensureConfirmationKitVisibility +} from './kit-runtime.js' function walkRules(rules, fn) { const list = Array.isArray(rules) ? rules : [] @@ -28,7 +34,11 @@ export function prepareMobileFormSchema(formSchemaJson) { : Array.isArray(schema) ? schema : [] - const rule = ensureOaUploadRuleProps(rawRule) + const rule = ensureConfirmationKitVisibility( + ensureOwnerFieldRuleProps( + ensureOvertimeKitRuleProps(ensureLeaveKitRuleProps(ensureOaUploadRuleProps(rawRule))) + ) + ) const option = { ...oaFormOption, ...(schema.option || {}), diff --git a/mobile-oa-h5/src/views/OaStart.vue b/mobile-oa-h5/src/views/OaStart.vue index 53219ae2..cb915999 100644 --- a/mobile-oa-h5/src/views/OaStart.vue +++ b/mobile-oa-h5/src/views/OaStart.vue @@ -49,9 +49,30 @@ import { fetchOaInstanceDraftDetail, fetchSubmitOaInstanceDraft, fetchStartOaInstance, - fetchUserList + fetchUserList, + fetchCurrentUser, + fetchCnHolidayRange } from '../api/oa.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' const route = useRoute() @@ -70,9 +91,19 @@ const flowResolved = ref(false) const flowPendingMessage = ref('必填信息填写后,流程将自动显示') const initiatorSelectByNode = ref({}) const userOptions = ref([]) +const currentUser = ref(null) let previewTimer = null 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) { try { @@ -87,6 +118,14 @@ function closeToMini(payload) { showToast((payload && payload.message) || '已完成(非小程序环境)') } +async function loadCurrentUser() { + try { + currentUser.value = (await fetchCurrentUser()) || null + } catch { + currentUser.value = null + } +} + async function loadUsers() { try { const pageSize = 100 @@ -101,11 +140,13 @@ async function loadUsers() { page += 1 if (page > 50) break } - userOptions.value = raw.map((u) => ({ - id: String(u.id || ''), - nickName: u.nickName || '', - userNo: u.userNo || '' - })).filter((u) => u.id) + userOptions.value = raw + .map((u) => ({ + id: String(u.id || ''), + nickName: u.nickName || '', + userNo: u.userNo || '' + })) + .filter((u) => u.id) } catch { userOptions.value = [] } @@ -155,7 +196,227 @@ function scheduleFlowPreview() { }, 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) appMeta.value = { id: appId, @@ -164,6 +425,16 @@ async function loadByAppId(appId) { const prepared = prepareMobileFormSchema(detail?.formSchemaJson) formRule.value = prepared.rule 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() } @@ -171,6 +442,7 @@ async function bootstrap() { detailLoading.value = true skipPreviewWatch = true try { + await loadCurrentUser() const appId = String(route.query.appId || '').trim() const draftId = String(route.query.draftId || '').trim() if (draftId) { @@ -178,17 +450,18 @@ async function bootstrap() { editingDraftId.value = draftId const aid = String(d?.appId || '').trim() if (!aid) throw new Error('草稿缺少 appId') - formData.value = d.formData && typeof d.formData === 'object' ? { ...d.formData } : {} initiatorSelectByNode.value = d.initiatorSelectByNode && typeof d.initiatorSelectByNode === 'object' ? { ...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) { - formData.value = {} initiatorSelectByNode.value = {} editingDraftId.value = '' - await loadByAppId(appId) + await loadByAppId(appId, { seedData: {}, applyDefaults: true }) } else { showToast('缺少 appId') } @@ -288,7 +561,20 @@ async function submit() { } watch( - [formData, initiatorSelectByNode], + formData, + () => { + if (skipPreviewWatch) return + syncLeaveKitDurations() + syncTripKitDurations() + syncConfirmationKitDates() + void syncOvertimeKitDurations() + scheduleFlowPreview() + }, + { deep: true } +) + +watch( + initiatorSelectByNode, () => { if (skipPreviewWatch) return scheduleFlowPreview()