ソースを参照

feat(oa): add overtime kit form components and designer registration

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 ヶ月 前
親
コミット
b3d267e9ca

+ 34 - 0
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

+ 23 - 0
web-admin/src/components/oa-approval/OaOvertimeKit.vue

@@ -0,0 +1,23 @@
+<!-- 加班套件:仅组合容器;时长手动填写,无自动计算 -->
+<template>
+  <div class="oa-overtime-kit" v-bind="$attrs">
+    <slot />
+  </div>
+</template>
+
+<script setup lang="ts">
+  defineOptions({ name: 'OaOvertimeKit', inheritAttrs: false })
+
+  defineProps<{
+    personField?: string
+    startField?: string
+    endField?: string
+    durationField?: string
+  }>()
+</script>
+
+<style scoped lang="scss">
+  .oa-overtime-kit {
+    width: 100%;
+  }
+</style>

+ 26 - 0
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
+}

+ 111 - 0
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 []
+  }
+}

+ 105 - 0
web-admin/src/utils/oa-overtime-kit-runtime.ts

@@ -0,0 +1,105 @@
+/** 加班套件运行时:时长仅手动填写,补齐数字框左对齐 */
+
+function asRules(schema: unknown): Record<string, unknown>[] {
+  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, unknown>): 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<string, any>[]
+  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<string, unknown>[] = []
+  const walk = (rules: Record<string, unknown>[]) => {
+    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')
+}