diff --git a/docs/superpowers/specs/2026-07-30-oa-overtime-kit-design.md b/docs/superpowers/specs/2026-07-30-oa-overtime-kit-design.md new file mode 100644 index 00000000..2e7af574 --- /dev/null +++ b/docs/superpowers/specs/2026-07-30-oa-overtime-kit-design.md @@ -0,0 +1,34 @@ +# OA 表单设计 · 加班套件(Overtime Kit)设计文档 + +**日期**: 2026-07-30 +**状态**: 已实现 +**范围**: `web-admin` 审批设计器「套件」Tab + 发起/详情运行时;发布白名单 +**前置**: 请假套件 `docs/superpowers/specs/2026-07-30-oa-leave-kit-design.md` + +--- + +## 1. 目标 + +套件 Tab 增加 **加班套件**:一键插入 4 个字段。 + +| 字段 | 控件 | 说明 | +|------|------|------| +| 加班人 | `person`(多选,与人员组件一致) | 必填 | +| 开始时间 | `datePicker` datetime `YYYY-MM-DD HH:mm` | 必填 | +| 结束时间 | `datePicker` datetime `YYYY-MM-DD HH:mm` | 必填 | +| 时长(h) | `inputNumber` 1 位小数 | **仅手动填写**,无自动计算 | + +## 2. 实现要点 + +- 容器类型:`overtimeKit`(布局类,不计可填字段数) +- 运行时不渲染套件边框/标题(同请假) +- 无套件级工时属性、无前后端时长重算 +- 打印 / 发布白名单纳入 `overtimeKit` + +## 3. 文件 + +- `OaOvertimeKit.vue` +- `oa-overtime-kit-rule.ts` / `oa-form-create-overtime-kit.ts` / `oa-overtime-kit-runtime.ts` +- `main.ts` / `oa-fc-host/main.ts` 注册 +- `oa_publish.py` `LAYOUT_TYPES` +- `oa-approval-print.ts` 布局 unwrap diff --git a/web-admin/src/components/oa-approval/OaOvertimeKit.vue b/web-admin/src/components/oa-approval/OaOvertimeKit.vue new file mode 100644 index 00000000..a3041a1f --- /dev/null +++ b/web-admin/src/components/oa-approval/OaOvertimeKit.vue @@ -0,0 +1,23 @@ + + + + + + diff --git a/web-admin/src/utils/oa-form-create-overtime-kit.ts b/web-admin/src/utils/oa-form-create-overtime-kit.ts new file mode 100644 index 00000000..919b2ce3 --- /dev/null +++ b/web-admin/src/utils/oa-form-create-overtime-kit.ts @@ -0,0 +1,26 @@ +import FcDesigner from '@form-create/designer' +import OaOvertimeKit from '@/components/oa-approval/OaOvertimeKit.vue' +import { overtimeKitDragRule } from '@/utils/oa-overtime-kit-rule' + +const FLAG = '__oaOvertimeKitConfigured' + +export function setupOaOvertimeKit(formCreate?: { component?: (name: string, c: unknown) => void }) { + if ((FcDesigner as any)[FLAG]) return + + if (typeof FcDesigner.addMenu === 'function') { + try { + FcDesigner.addMenu({ name: 'kit', title: '套件', list: [] }) + } catch { + /* kit menu may already exist */ + } + } + + FcDesigner.component('overtimeKit', OaOvertimeKit) + if (typeof formCreate?.component === 'function') { + formCreate.component('overtimeKit', OaOvertimeKit) + } + if (typeof FcDesigner.addDragRule === 'function') { + FcDesigner.addDragRule(overtimeKitDragRule) + } + ;(FcDesigner as any)[FLAG] = true +} diff --git a/web-admin/src/utils/oa-overtime-kit-rule.ts b/web-admin/src/utils/oa-overtime-kit-rule.ts new file mode 100644 index 00000000..b1ef4d56 --- /dev/null +++ b/web-admin/src/utils/oa-overtime-kit-rule.ts @@ -0,0 +1,111 @@ +/** FormCreate Designer DragRule:加班套件 */ + +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) + + 'o' + ) +} + +function buildOvertimeKitChildren() { + const personField = uniqueId() + const startField = uniqueId() + const endField = uniqueId() + const durationField = uniqueId() + return { + personField, + startField, + endField, + durationField, + children: [ + { + type: 'person', + field: personField, + title: '加班人', + info: '', + $required: true, + props: { + _fc_kit_field: 'overtimePerson', + multiple: true + } + }, + { + type: 'datePicker', + field: startField, + title: '开始时间', + info: '', + $required: true, + props: { + _fc_kit_field: 'overtimeStart', + type: 'datetime', + format: 'YYYY-MM-DD HH:mm', + valueFormat: 'YYYY-MM-DD HH:mm', + placeholder: '请选择开始时间' + } + }, + { + type: 'datePicker', + field: endField, + title: '结束时间', + info: '', + $required: true, + props: { + _fc_kit_field: 'overtimeEnd', + type: 'datetime', + format: 'YYYY-MM-DD HH:mm', + valueFormat: 'YYYY-MM-DD HH:mm', + placeholder: '请选择结束时间' + } + }, + { + type: 'inputNumber', + field: durationField, + title: '时长(h)', + info: '请手动填写加班时长', + $required: true, + props: { + _fc_kit_field: 'overtimeDuration', + precision: 1, + min: 0, + controlsPosition: 'right', + align: 'left' + }, + class: 'oa-overtime-duration' + } + ] + } +} + +/** FormCreate Designer DragRule:加班套件 */ +export const overtimeKitDragRule = { + menu: 'kit' as const, + icon: 'icon-date', + label: '加班', + name: 'overtimeKit', + input: false, + drag: true, + dragBtn: true, + mask: false, + inside: true, + rule() { + const built = buildOvertimeKitChildren() + return { + type: 'overtimeKit', + props: { + personField: built.personField, + startField: built.startField, + endField: built.endField, + durationField: built.durationField + }, + children: built.children + } + }, + props() { + return [] + } +} diff --git a/web-admin/src/utils/oa-overtime-kit-runtime.ts b/web-admin/src/utils/oa-overtime-kit-runtime.ts new file mode 100644 index 00000000..caab06cf --- /dev/null +++ b/web-admin/src/utils/oa-overtime-kit-runtime.ts @@ -0,0 +1,105 @@ +/** 加班套件运行时:时长仅手动填写,补齐数字框左对齐 */ + +function asRules(schema: unknown): Record[] { + if (Array.isArray(schema)) return schema.filter((r) => r && typeof r === 'object') as any + if (schema && typeof schema === 'object') { + const s = schema as any + const rules = s.rule || s.rules || s.fields + if (Array.isArray(rules)) return rules.filter((r: any) => r && typeof r === 'object') + if (s.type || s.field) return [s] + } + return [] +} + +function resolveDurationField(kit: Record): string | undefined { + const props = (kit.props && typeof kit.props === 'object' ? kit.props : {}) as Record< + string, + any + > + if (props.durationField) return String(props.durationField) + const children = ( + Array.isArray(kit.children) + ? kit.children + : Array.isArray(props.rule) + ? props.rule + : [] + ) as Record[] + for (const c of children) { + if (!c?.field) continue + if (String(c.props?._fc_kit_field || '') === 'overtimeDuration') return String(c.field) + const t = String(c.type || '') + if ((t === 'inputNumber' || t === 'number') && String(c.title || '').includes('时长')) { + return String(c.field) + } + } + return undefined +} + +/** + * 运行时补齐加班套件子规则:时长数字框默认左对齐。 + */ +export function ensureOvertimeKitRuleProps(rules: unknown): any[] { + if (!Array.isArray(rules)) return [] + return rules.map((raw) => { + if (!raw || typeof raw !== 'object') return raw + const rule: any = { ...(raw as object) } + if (String(rule.type || '') === 'overtimeKit') { + const durationField = resolveDurationField(rule) + const patchChildren = (list: any[]): any[] => + list.map((c) => { + if (!c || typeof c !== 'object') return c + const child: any = { ...c } + const t = String(child.type || '') + const isDuration = + (durationField && child.field === durationField) || + ((t === 'inputNumber' || t === 'number') && + String(child.title || '').includes('时长')) + 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 + }) +} + +/** 供类型检查 / 后续扩展:当前 schema 是否含加班套件 */ +export function hasOvertimeKit(schema: unknown): boolean { + const flat: Record[] = [] + const walk = (rules: Record[]) => { + for (const r of rules) { + flat.push(r) + if (Array.isArray(r.children)) walk(r.children as any) + const props = r.props as any + if (props && Array.isArray(props.rule)) walk(props.rule) + } + } + walk(asRules(schema)) + return flat.some((r) => String(r.type || '') === 'overtimeKit') +}