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')
+}