diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py
index 0332c39e..4d4822d4 100644
--- a/backend/app/api/admin.py
+++ b/backend/app/api/admin.py
@@ -255,6 +255,7 @@ class UserCreateBody(BaseModel):
phone_no: Optional[str] = Field("", alias="phoneNo")
hire_date: Optional[str] = Field(None, alias="hireDate")
probation_months: Optional[int] = Field(None, alias="probationMonths")
+ last_work_day: Optional[str] = Field(None, alias="lastWorkDay")
sex: Optional[str] = Field("0", alias="sex")
avatar: Optional[str] = Field(None, alias="avatar")
status: Optional[str] = Field("1", alias="status")
@@ -273,6 +274,7 @@ class UserUpdateBody(BaseModel):
phone_no: Optional[str] = Field(None, alias="phoneNo")
hire_date: Optional[str] = Field(None, alias="hireDate")
probation_months: Optional[int] = Field(None, alias="probationMonths")
+ last_work_day: Optional[str] = Field(None, alias="lastWorkDay")
sex: Optional[str] = Field(None, alias="sex")
avatar: Optional[str] = Field(None, alias="avatar")
status: Optional[str] = Field(None, alias="status")
@@ -384,6 +386,7 @@ async def create_user(
phone_no=body.phone_no,
hire_date=body.hire_date,
probation_months=body.probation_months,
+ last_work_day=body.last_work_day,
sex=body.sex,
avatar=body.avatar,
status=body.status,
@@ -427,6 +430,8 @@ async def update_user(
update_kwargs["hire_date"] = payload.get("hire_date")
if "probation_months" in payload:
update_kwargs["probation_months"] = payload.get("probation_months")
+ if "last_work_day" in payload:
+ update_kwargs["last_work_day"] = payload.get("last_work_day")
obj = user_svc.update_user(db, user_id, **update_kwargs)
return success_response(user_svc._row_to_dict(obj), message="更新成功")
except ValueError as e:
diff --git a/backend/app/api/public.py b/backend/app/api/public.py
index 30b40382..bc34c81a 100644
--- a/backend/app/api/public.py
+++ b/backend/app/api/public.py
@@ -61,6 +61,9 @@ def _build_user_info_payload(user: SysUser, web_role: str, role_names: list = No
"email": (getattr(user, "email", None) or "").strip() or None,
"hireDate": user.hire_date.strftime("%Y-%m-%d") if getattr(user, "hire_date", None) else None,
"probationMonths": getattr(user, "probation_months", None),
+ "lastWorkDay": user.last_work_day.strftime("%Y-%m-%d")
+ if getattr(user, "last_work_day", None)
+ else None,
"sex": getattr(user, "sex", None) or "0",
"remark": (getattr(user, "remark", None) or "").strip() or None,
"role": web_role,
diff --git a/backend/app/crud/user.py b/backend/app/crud/user.py
index 60c9e37c..b204ecdc 100644
--- a/backend/app/crud/user.py
+++ b/backend/app/crud/user.py
@@ -48,6 +48,9 @@ def _row_to_dict(u: SysUser, dept_name: str = "") -> Dict[str, Any]:
"probationMonths": (
u.probation_months if getattr(u, "probation_months", None) is not None else None
),
+ "lastWorkDay": u.last_work_day.strftime("%Y-%m-%d")
+ if getattr(u, "last_work_day", None)
+ else "",
"sex": u.sex or "0",
"avatar": u.avatar or "",
"status": u.status,
@@ -311,6 +314,7 @@ def create_user(
phone_no: str = "",
hire_date: Optional[Union[str, date]] = None,
probation_months: Optional[int] = None,
+ last_work_day: Optional[Union[str, date]] = None,
sex: str = "0",
avatar: Optional[str] = None,
status: str = "1",
@@ -342,6 +346,7 @@ def create_user(
phone_no=(phone_no or "").strip(),
hire_date=_parse_hire_date(hire_date),
probation_months=probation_months,
+ last_work_day=_parse_hire_date(last_work_day),
sex=sex or "0",
avatar=avatar,
password=_hash_password(pw),
@@ -375,6 +380,7 @@ def update_user(
phone_no: Optional[str] = None,
hire_date: Any = ...,
probation_months: Any = ...,
+ last_work_day: Any = ...,
sex: Optional[str] = None,
avatar: Optional[str] = None,
status: Optional[str] = None,
@@ -406,6 +412,8 @@ def update_user(
obj.probation_months = None
else:
obj.probation_months = int(probation_months)
+ if last_work_day is not ...:
+ obj.last_work_day = _parse_hire_date(last_work_day)
if sex is not None:
obj.sex = sex
if avatar is not None:
diff --git a/backend/app/models/models.py b/backend/app/models/models.py
index e9fe5dd6..ed358254 100644
--- a/backend/app/models/models.py
+++ b/backend/app/models/models.py
@@ -33,6 +33,7 @@ class SysUser(Base):
phone_no = Column(String(11), nullable=False, default="", comment="手机号码")
hire_date = Column(Date, nullable=True, comment="入职日期")
probation_months = Column(Integer, nullable=True, comment="试用期月数")
+ last_work_day = Column(Date, nullable=True, comment="最后工作日")
sex = Column(CHAR(1), nullable=True, default="0", comment="用户性别")
avatar = Column(String(512), nullable=True, comment="头像")
password = Column("password", String(100), nullable=False, default="", comment="密码")
diff --git a/backend/app/services/oa_engine.py b/backend/app/services/oa_engine.py
index 7b69a6d1..2ccdf5f2 100644
--- a/backend/app/services/oa_engine.py
+++ b/backend/app/services/oa_engine.py
@@ -30,6 +30,7 @@ from app.services.oa_assignee import (
)
from app.services.oa_formula import FormulaError, apply_formulas_and_linkages, eval_formula
from app.services.oa_confirmation_kit_sync import apply_confirmation_kit_user_sync
+from app.services.oa_resignation_kit_sync import apply_resignation_kit_user_sync
import logging
@@ -1647,6 +1648,12 @@ def advance_from(
logger.exception(
"confirmation kit user sync failed instance=%s", getattr(inst, "id", None)
)
+ try:
+ apply_resignation_kit_user_sync(db, inst)
+ except Exception:
+ logger.exception(
+ "resignation kit user sync failed instance=%s", getattr(inst, "id", None)
+ )
return
if ntype == "condition":
cur = next_node(cur)
diff --git a/backend/app/services/oa_publish.py b/backend/app/services/oa_publish.py
index 2182ab1f..afd9feed 100644
--- a/backend/app/services/oa_publish.py
+++ b/backend/app/services/oa_publish.py
@@ -60,6 +60,8 @@ LAYOUT_TYPES = frozenset(
"object",
"leaveKit",
"confirmationKit",
+ "overtimeKit",
+ "resignationKit",
}
)
diff --git a/backend/app/services/oa_resignation_kit_sync.py b/backend/app/services/oa_resignation_kit_sync.py
new file mode 100644
index 00000000..f0d47c4d
--- /dev/null
+++ b/backend/app/services/oa_resignation_kit_sync.py
@@ -0,0 +1,135 @@
+"""离职套件:审批通过后回写发起人 last_work_day。"""
+from __future__ import annotations
+
+import json
+import logging
+import re
+from datetime import date, datetime
+from typing import Any, Dict, List, Optional
+
+from sqlalchemy.orm import Session
+
+from app.models.models import OaAppVersion, OaInstance, SysUser
+
+logger = logging.getLogger(__name__)
+
+_YMD = re.compile(r"^(\d{4})-(\d{2})-(\d{2})$")
+
+
+def _loads(raw: Any, default: Any = None) -> Any:
+ if raw is None or raw == "":
+ return default
+ if isinstance(raw, (dict, list)):
+ return raw
+ try:
+ return json.loads(raw)
+ except Exception:
+ return default
+
+
+def _extract_rules(form_schema: Any) -> List[Dict[str, Any]]:
+ if form_schema is None:
+ return []
+ if isinstance(form_schema, list):
+ return [r for r in form_schema if isinstance(r, dict)]
+ if isinstance(form_schema, dict):
+ rules = form_schema.get("rule") or form_schema.get("rules") or form_schema.get("fields")
+ if isinstance(rules, list):
+ return [r for r in rules if isinstance(r, dict)]
+ if form_schema.get("type") or form_schema.get("field"):
+ return [form_schema]
+ return []
+
+
+def _walk(rules: List[Dict[str, Any]], out: List[Dict[str, Any]]) -> None:
+ for rule in rules:
+ out.append(rule)
+ children = rule.get("children") or (rule.get("props") or {}).get("rule") or []
+ if isinstance(children, list):
+ _walk([c for c in children if isinstance(c, dict)], out)
+
+
+def _last_work_day_field(kit: Dict[str, Any]) -> Optional[str]:
+ props = kit.get("props") if isinstance(kit.get("props"), dict) else {}
+ if props.get("lastWorkDayField"):
+ return str(props["lastWorkDayField"])
+ children = kit.get("children") or props.get("rule") or []
+ if isinstance(children, list):
+ for c in children:
+ if not isinstance(c, dict) or not c.get("field"):
+ continue
+ cprops = c.get("props") if isinstance(c.get("props"), dict) else {}
+ if str(cprops.get("_fc_kit_field") or "") == "lastWorkDay":
+ return str(c["field"])
+ return None
+
+
+def _parse_ymd(value: Any) -> Optional[date]:
+ if value is None or value == "":
+ return None
+ if isinstance(value, datetime):
+ return value.date()
+ if isinstance(value, date):
+ return value
+ s = str(value).strip()[:10]
+ if not _YMD.match(s):
+ return None
+ try:
+ return datetime.strptime(s, "%Y-%m-%d").date()
+ except ValueError:
+ return None
+
+
+def extract_last_work_day(schema: Any, form_data: Dict[str, Any]) -> Optional[date]:
+ flat: List[Dict[str, Any]] = []
+ _walk(_extract_rules(schema), flat)
+ for rule in flat:
+ if str(rule.get("type") or "") != "resignationKit":
+ continue
+ field = _last_work_day_field(rule)
+ if not field:
+ continue
+ parsed = _parse_ymd(form_data.get(field))
+ if parsed is not None:
+ return parsed
+ return None
+
+
+def apply_resignation_kit_user_sync(db: Session, inst: OaInstance) -> None:
+ try:
+ if not inst or not getattr(inst, "id", None):
+ return
+ form_data = _loads(getattr(inst, "form_data_json", None), {}) or {}
+ if not isinstance(form_data, dict):
+ form_data = {}
+ ver = (
+ db.query(OaAppVersion)
+ .filter(OaAppVersion.id == inst.app_version_id, OaAppVersion.del_flag == 0)
+ .first()
+ )
+ schema = _loads(getattr(ver, "form_schema_json", None) if ver else None, {}) or {}
+ last_day = extract_last_work_day(schema, form_data)
+ if last_day is None:
+ return
+ user = (
+ db.query(SysUser)
+ .filter(
+ SysUser.id == inst.initiator_user_id,
+ SysUser.tenant_no == inst.tenant_no,
+ SysUser.del_flag == 0,
+ )
+ .first()
+ )
+ if not user:
+ logger.warning(
+ "resignation kit sync: initiator not found id=%s",
+ inst.initiator_user_id,
+ )
+ return
+ user.last_work_day = last_day
+ user.update_time = datetime.now()
+ db.add(user)
+ except Exception:
+ logger.exception(
+ "resignation kit user sync failed instance=%s", getattr(inst, "id", None)
+ )
diff --git a/backend/tests/test_oa_resignation_kit_sync.py b/backend/tests/test_oa_resignation_kit_sync.py
new file mode 100644
index 00000000..83519454
--- /dev/null
+++ b/backend/tests/test_oa_resignation_kit_sync.py
@@ -0,0 +1,39 @@
+from datetime import date
+
+from app.services.oa_resignation_kit_sync import extract_last_work_day
+
+
+def test_extract_last_work_day():
+ schema = {
+ "rule": [
+ {
+ "type": "resignationKit",
+ "props": {"lastWorkDayField": "f_last"},
+ "children": [
+ {
+ "type": "datePicker",
+ "field": "f_last",
+ "props": {"_fc_kit_field": "lastWorkDay"},
+ }
+ ],
+ }
+ ]
+ }
+ assert extract_last_work_day(schema, {"f_last": "2026-08-01"}) == date(2026, 8, 1)
+
+
+def test_empty_skipped():
+ schema = {
+ "rule": [
+ {
+ "type": "resignationKit",
+ "props": {"lastWorkDayField": "f_last"},
+ "children": [],
+ }
+ ]
+ }
+ assert extract_last_work_day(schema, {"f_last": ""}) is None
+
+
+def test_no_kit():
+ assert extract_last_work_day({"rule": [{"type": "input", "field": "x"}]}, {"x": 1}) is None
diff --git a/docs/sql/sys_user_last_work_day.sql b/docs/sql/sys_user_last_work_day.sql
new file mode 100644
index 00000000..c58351e5
--- /dev/null
+++ b/docs/sql/sys_user_last_work_day.sql
@@ -0,0 +1,4 @@
+-- docs/sql/sys_user_last_work_day.sql
+-- 用户表补齐最后工作日(离职套件主数据)
+ALTER TABLE `sys_user`
+ ADD COLUMN `last_work_day` date DEFAULT NULL COMMENT '最后工作日' AFTER `probation_months`;
diff --git a/docs/superpowers/specs/2026-07-30-oa-resignation-kit-design.md b/docs/superpowers/specs/2026-07-30-oa-resignation-kit-design.md
new file mode 100644
index 00000000..37ee264f
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-30-oa-resignation-kit-design.md
@@ -0,0 +1,202 @@
+# OA 表单设计 · 离职套件(Resignation Kit)设计文档
+
+**日期**: 2026-07-30
+**状态**: 已实现
+**范围**: `web-admin` 用户管理补齐最后工作日;审批套件 Tab 离职套件;发起默认入职日期;`backend` 审批通过后回写发起人 `last_work_day`
+**界面模式**: 对齐请假/转正套件;视觉 Fresh Mint
+**前置**:
+- `docs/superpowers/specs/2026-07-30-oa-leave-kit-design.md`
+- `docs/superpowers/specs/2026-07-30-oa-confirmation-kit-design.md`(回写模式)
+
+---
+
+## 1. 背景与目标
+
+套件 Tab 增加 **离职套件**:采集入职日期、最后工作日、离职原因及备注;审批通过后将最后工作日回写发起人用户主数据。
+
+### 1.1 目标
+
+- `sys_user` 增补 **最后工作日**;用户管理新增/编辑/详情可维护。
+- 拖入离职套件得到 4 个子字段。
+- 发起时入职日期默认申请人(可改)。
+- 离职原因内置 5 项,设计器可维护 options。
+- 审批 **全部通过** 后仅回写发起人 `last_work_day`。
+
+### 1.2 非目标(首期)
+
+- 通过后自动停用账号。
+- 入职日期、离职原因、备注回写用户表。
+- 「最后工作日 ≥ 入职日期」交叉校验。
+- 代他人离职并回写他人主数据。
+
+---
+
+## 2. 产品决策(已确认)
+
+| 项 | 决策 |
+|----|------|
+| 实现方案 | 套件容器 + 子字段拼装 |
+| 容器类型 | `resignationKit` |
+| 回写时机 | 审批全部通过 |
+| 回写对象 | 始终发起人 |
+| 回写字段 | 仅最后工作日 |
+| 账号状态 | 不动 |
+| 用户管理 | 最后工作日可新增/编辑/详情 |
+| 入职日期 | 默认申请人,可改,不回写 |
+| 最后工作日 | 默认必填(设计器可改);手填 |
+| 交叉校验 | 首期不做 |
+| 原因/备注 | 仅审批单 |
+
+---
+
+## 3. 信息模型
+
+### 3.1 心智模型
+
+```
+┌─ 离职套件(resignationKit)──────────────────────────┐
+│ · datePicker 入职日期 ← userInfo.hireDate │
+│ · datePicker 最后工作日 ← 手填;通过后回写 │
+│ · select 离职原因 ← 5 默认项,可配置 │
+│ · textarea 离职原因备注 │
+└──────────────────────────────────────────────────────┘
+```
+
+运行时不渲染套件边框/标题。
+
+### 3.2 子字段
+
+| 语义 key | title | type | 必填默认 | 回写 |
+|----------|-------|------|----------|------|
+| `hireDate` | 入职日期 | datePicker 仅日期 | 是 | ✗ |
+| `lastWorkDay` | 最后工作日 | datePicker 仅日期 | 是 | ✓ → `last_work_day` |
+| `resignReason` | 离职原因 | select | 是 | ✗ |
+| `resignRemark` | 离职原因备注 | textarea | 否 | ✗ |
+
+离职原因默认 options:
+
+- 家庭原因
+- 个人原因
+- 发展原因
+- 合同到期不续签
+- 其他
+
+语义识别:`props._fc_kit_field`;容器 props 可挂各 `*Field` id。
+
+### 3.3 Schema 示意
+
+```json
+{
+ "type": "resignationKit",
+ "props": {
+ "hireDateField": "...",
+ "lastWorkDayField": "...",
+ "resignReasonField": "...",
+ "resignRemarkField": "..."
+ },
+ "children": [
+ {
+ "type": "datePicker",
+ "title": "入职日期",
+ "props": { "_fc_kit_field": "hireDate", "type": "date", "valueFormat": "YYYY-MM-DD" },
+ "$required": true
+ },
+ {
+ "type": "datePicker",
+ "title": "最后工作日",
+ "props": { "_fc_kit_field": "lastWorkDay", "type": "date", "valueFormat": "YYYY-MM-DD" },
+ "$required": true
+ },
+ {
+ "type": "select",
+ "title": "离职原因",
+ "options": [
+ { "label": "家庭原因", "value": "家庭原因" },
+ { "label": "个人原因", "value": "个人原因" },
+ { "label": "发展原因", "value": "发展原因" },
+ { "label": "合同到期不续签", "value": "合同到期不续签" },
+ { "label": "其他", "value": "其他" }
+ ],
+ "props": { "_fc_kit_field": "resignReason" },
+ "$required": true
+ },
+ {
+ "type": "textarea",
+ "title": "离职原因备注",
+ "props": { "_fc_kit_field": "resignRemark", "rows": 3 },
+ "$required": false
+ }
+ ]
+}
+```
+
+---
+
+## 4. 用户主数据
+
+### 4.1 增列
+
+```sql
+ALTER TABLE `sys_user`
+ ADD COLUMN `last_work_day` date DEFAULT NULL COMMENT '最后工作日' AFTER `probation_months`;
+```
+
+(若库中无 `probation_months`,则 AFTER `hire_date` 或 `phone_no`。)
+
+同步:`SysUser`、`_row_to_dict`(`lastWorkDay`)、Create/Update Body、用户管理表单与详情、`userInfo` 可选带出 `lastWorkDay`(套件默认不依赖此项)。
+
+---
+
+## 5. 运行时
+
+### 5.1 发起默认
+
+仅对空的入职日期写入 `userInfo.hireDate`。其余字段不默认。
+
+### 5.2 提交 / 发布
+
+- 可见必填按 FormCreate 校验。
+- `LAYOUT_TYPES` 纳入 `resignationKit`。
+- 打印 unwrap 同其它套件。
+
+### 5.3 通过后回写
+
+挂在实例 `status = completed` 后(可与转正 sync 并列调用):
+
+`apply_resignation_kit_user_sync(db, inst)`
+
+- 扫描 `resignationKit` → 取 `lastWorkDay`
+- 有合法 `YYYY-MM-DD` 则更新发起人 `last_work_day`
+- 空 / 非法:跳过 + log;异常不回滚通过状态
+
+---
+
+## 6. 建议文件落点
+
+| 区域 | 路径 |
+|------|------|
+| SQL | `docs/sql/sys_user_last_work_day.sql` |
+| 模型/CRUD/API/用户管理 | 同转正补字段路径 |
+| 容器 | `OaResignationKit.vue` |
+| DragRule / 注册 | `oa-resignation-kit-rule.ts`、`oa-form-create-resignation-kit.ts` |
+| 默认值 | `oa-resignation-kit-runtime.ts` |
+| 回写 | `oa_resignation_kit_sync.py` + `oa_engine` completed 钩子 |
+| 发布/打印 | `oa_publish.py`、`oa-approval-print.ts` |
+| 设计器拖入刷新 | `oa-fc-host/App.vue`(纳入 kit 名列表) |
+
+---
+
+## 7. 验收标准
+
+1. 用户管理可维护最后工作日。
+2. 套件 Tab 可拖「离职」;原因默认 5 项且设计器可改。
+3. 发起:入职日期自动带出可改;最后工作日/原因/备注按规则校验。
+4. 通过前主数据最后工作日不变;通过后写入发起人;空不写。
+5. 入职日期、原因、备注不回写;账号状态不变。
+
+---
+
+## 8. 开放细节(不阻塞)
+
+- `userInfo` 是否带出 `lastWorkDay`:可选,套件不依赖。
+- 与转正 sync 共用一个 hook 文件或并列两个函数:实现任选,逻辑分离即可。
diff --git a/web-admin/src/api/admin.ts b/web-admin/src/api/admin.ts
index 3ab9218e..c2cf5b94 100644
--- a/web-admin/src/api/admin.ts
+++ b/web-admin/src/api/admin.ts
@@ -134,6 +134,7 @@ export function fetchCreateUser(data: {
phoneNo?: string
hireDate?: string
probationMonths?: number | null
+ lastWorkDay?: string
sex?: string
avatar?: string
status?: string
@@ -159,6 +160,7 @@ export function fetchUpdateUser(
phoneNo: string
hireDate: string | null
probationMonths: number | null
+ lastWorkDay: string | null
sex: string
avatar: string
status: string
diff --git a/web-admin/src/components/oa-approval/OaResignationKit.vue b/web-admin/src/components/oa-approval/OaResignationKit.vue
new file mode 100644
index 00000000..05b91951
--- /dev/null
+++ b/web-admin/src/components/oa-approval/OaResignationKit.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/web-admin/src/main.ts b/web-admin/src/main.ts
index 7b96917d..328663bc 100644
--- a/web-admin/src/main.ts
+++ b/web-admin/src/main.ts
@@ -26,6 +26,8 @@ import { setupOaPostSelect } from '@/utils/oa-form-create-post'
import { setupOaReceiveAccountSelect } from '@/utils/oa-form-create-receive-account'
import { setupOaLeaveKit } from '@/utils/oa-form-create-leave-kit'
import { setupOaConfirmationKit } from '@/utils/oa-form-create-confirmation-kit'
+import { setupOaOvertimeKit } from '@/utils/oa-form-create-overtime-kit'
+import { setupOaResignationKit } from '@/utils/oa-form-create-resignation-kit'
// FormCreate 按字符串解析 el-input / fc-radio 等,主应用是按需自动导入,不会全局注册;
// 必须挂上官方 auto-import,否则发起审批/详情里动态表单控件会残缺(如单选项只剩文字)。
@@ -42,6 +44,8 @@ setupOaPostSelect(FcDesigner.formCreate)
setupOaReceiveAccountSelect(FcDesigner.formCreate)
setupOaLeaveKit(FcDesigner.formCreate)
setupOaConfirmationKit(FcDesigner.formCreate)
+setupOaOvertimeKit(FcDesigner.formCreate)
+setupOaResignationKit(FcDesigner.formCreate)
function maskSecret(value?: string) {
if (!value) return '(empty)'
diff --git a/web-admin/src/oa-fc-host/App.vue b/web-admin/src/oa-fc-host/App.vue
index ed0314d1..6a5444eb 100644
--- a/web-admin/src/oa-fc-host/App.vue
+++ b/web-admin/src/oa-fc-host/App.vue
@@ -7,12 +7,13 @@
height="100%"
:config="designerConfig"
:locale="fcLocale"
+ @drag="onDesignerDrag"
/>
@@ -488,4 +627,38 @@
.oa-fc-host ._fc-r {
--el-color-primary: #10b981;
}
+
+ /* 自研三 Tab:组件 | 套件 | 大纲(对齐 FormCreate 原 Tab 样式,主色 Mint) */
+ .oa-fc-host ._fc-l-tabs {
+ padding: 0;
+ }
+
+ .oa-fc-host .oa-fc-l-tabs {
+ display: flex;
+ width: 100%;
+ height: 40px;
+ }
+
+ .oa-fc-host .oa-fc-l-tab {
+ flex: 1;
+ height: 40px;
+ box-sizing: border-box;
+ line-height: 40px;
+ text-align: center;
+ font-size: 14px;
+ font-weight: 600;
+ color: #303133;
+ cursor: pointer;
+ user-select: none;
+ }
+
+ .oa-fc-host .oa-fc-l-tab.active {
+ color: #059669;
+ border-bottom: 2px solid #10b981;
+ }
+
+ /* 套件 Tab:隐藏分组标题「套件」,只保留卡片列表 */
+ .oa-fc-host--left-kit ._fc-l-group > ._fc-l-title {
+ display: none;
+ }
diff --git a/web-admin/src/oa-fc-host/main.ts b/web-admin/src/oa-fc-host/main.ts
index 59c88c2d..384a8b84 100644
--- a/web-admin/src/oa-fc-host/main.ts
+++ b/web-admin/src/oa-fc-host/main.ts
@@ -23,6 +23,8 @@ import { setupOaPostSelect } from '@/utils/oa-form-create-post'
import { setupOaReceiveAccountSelect } from '@/utils/oa-form-create-receive-account'
import { setupOaLeaveKit } from '@/utils/oa-form-create-leave-kit'
import { setupOaConfirmationKit } from '@/utils/oa-form-create-confirmation-kit'
+import { setupOaOvertimeKit } from '@/utils/oa-form-create-overtime-kit'
+import { setupOaResignationKit } from '@/utils/oa-form-create-resignation-kit'
import App from './App.vue'
const app = createApp(App)
@@ -49,4 +51,6 @@ setupOaPostSelect(FcDesigner.formCreate)
setupOaReceiveAccountSelect(FcDesigner.formCreate)
setupOaLeaveKit(FcDesigner.formCreate)
setupOaConfirmationKit(FcDesigner.formCreate)
+setupOaOvertimeKit(FcDesigner.formCreate)
+setupOaResignationKit(FcDesigner.formCreate)
app.mount('#app')
diff --git a/web-admin/src/types/api.d.ts b/web-admin/src/types/api.d.ts
index fd5d39f8..a395881e 100644
--- a/web-admin/src/types/api.d.ts
+++ b/web-admin/src/types/api.d.ts
@@ -45,6 +45,7 @@ declare namespace Api {
phoneNo: string
hireDate?: string
probationMonths?: number | null
+ lastWorkDay?: string
sex: string
avatar: string
status: string
diff --git a/web-admin/src/utils/oa-approval-print.ts b/web-admin/src/utils/oa-approval-print.ts
index faee5104..6a5e027d 100644
--- a/web-admin/src/utils/oa-approval-print.ts
+++ b/web-admin/src/utils/oa-approval-print.ts
@@ -97,7 +97,9 @@ const SKIP_RULE_TYPES = new Set([
'html',
'vanCellGroup',
'leaveKit',
- 'confirmationKit'
+ 'confirmationKit',
+ 'overtimeKit',
+ 'resignationKit'
])
export function resolveStamp(status: string | undefined | null): {
diff --git a/web-admin/src/utils/oa-form-create-resignation-kit.ts b/web-admin/src/utils/oa-form-create-resignation-kit.ts
new file mode 100644
index 00000000..4fb8ae25
--- /dev/null
+++ b/web-admin/src/utils/oa-form-create-resignation-kit.ts
@@ -0,0 +1,28 @@
+import FcDesigner from '@form-create/designer'
+import OaResignationKit from '@/components/oa-approval/OaResignationKit.vue'
+import { resignationKitDragRule } from '@/utils/oa-resignation-kit-rule'
+
+const FLAG = '__oaResignationKitConfigured'
+
+export function setupOaResignationKit(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 {
+ /* already */
+ }
+ }
+
+ FcDesigner.component('resignationKit', OaResignationKit)
+ if (typeof formCreate?.component === 'function') {
+ formCreate.component('resignationKit', OaResignationKit)
+ }
+ if (typeof FcDesigner.addDragRule === 'function') {
+ FcDesigner.addDragRule(resignationKitDragRule)
+ }
+ ;(FcDesigner as any)[FLAG] = true
+}
diff --git a/web-admin/src/utils/oa-resignation-kit-rule.ts b/web-admin/src/utils/oa-resignation-kit-rule.ts
new file mode 100644
index 00000000..087d856f
--- /dev/null
+++ b/web-admin/src/utils/oa-resignation-kit-rule.ts
@@ -0,0 +1,117 @@
+/** 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) +
+ 'g'
+ )
+}
+
+export const RESIGN_REASON_OPTIONS = [
+ { label: '家庭原因', value: '家庭原因' },
+ { label: '个人原因', value: '个人原因' },
+ { label: '发展原因', value: '发展原因' },
+ { label: '合同到期不续签', value: '合同到期不续签' },
+ { label: '其他', value: '其他' }
+]
+
+function buildResignationKitChildren() {
+ const hireDateField = uniqueId()
+ const lastWorkDayField = uniqueId()
+ const resignReasonField = uniqueId()
+ const resignRemarkField = uniqueId()
+ return {
+ hireDateField,
+ lastWorkDayField,
+ resignReasonField,
+ resignRemarkField,
+ children: [
+ {
+ type: 'datePicker',
+ field: hireDateField,
+ title: '入职日期',
+ info: '',
+ $required: true,
+ props: {
+ _fc_kit_field: 'hireDate',
+ type: 'date',
+ format: 'YYYY-MM-DD',
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: '请选择入职日期'
+ }
+ },
+ {
+ type: 'datePicker',
+ field: lastWorkDayField,
+ title: '最后工作日',
+ info: '',
+ $required: true,
+ props: {
+ _fc_kit_field: 'lastWorkDay',
+ type: 'date',
+ format: 'YYYY-MM-DD',
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: '请选择最后工作日'
+ }
+ },
+ {
+ type: 'select',
+ field: resignReasonField,
+ title: '离职原因',
+ info: '',
+ $required: true,
+ options: RESIGN_REASON_OPTIONS.map((o) => ({ ...o })),
+ props: {
+ _fc_kit_field: 'resignReason',
+ placeholder: '请选择离职原因',
+ clearable: true
+ }
+ },
+ {
+ type: 'textarea',
+ field: resignRemarkField,
+ title: '离职原因备注',
+ info: '',
+ $required: false,
+ props: {
+ _fc_kit_field: 'resignRemark',
+ rows: 3,
+ placeholder: '可选'
+ }
+ }
+ ]
+ }
+}
+
+export const resignationKitDragRule = {
+ menu: 'kit' as const,
+ icon: 'icon-user',
+ label: '离职',
+ name: 'resignationKit',
+ input: false,
+ drag: true,
+ dragBtn: true,
+ mask: false,
+ inside: true,
+ rule() {
+ const built = buildResignationKitChildren()
+ return {
+ type: 'resignationKit',
+ props: {
+ hireDateField: built.hireDateField,
+ lastWorkDayField: built.lastWorkDayField,
+ resignReasonField: built.resignReasonField,
+ resignRemarkField: built.resignRemarkField
+ },
+ children: built.children
+ }
+ },
+ props() {
+ return []
+ }
+}
diff --git a/web-admin/src/utils/oa-resignation-kit-runtime.ts b/web-admin/src/utils/oa-resignation-kit-runtime.ts
new file mode 100644
index 00000000..833836e7
--- /dev/null
+++ b/web-admin/src/utils/oa-resignation-kit-runtime.ts
@@ -0,0 +1,80 @@
+/** 离职套件运行时:入职日期默认申请人 */
+
+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 walk(rules: Record[], out: Record[]) {
+ for (const r of rules) {
+ out.push(r)
+ if (Array.isArray(r.children)) {
+ walk(r.children as any, out)
+ }
+ const props = r.props as any
+ if (props && Array.isArray(props.rule)) walk(props.rule, out)
+ }
+}
+
+export type ResignationKitBinding = {
+ hireDateField?: string
+ lastWorkDayField?: string
+}
+
+function resolveBinding(kit: Record): ResignationKitBinding {
+ const props = (kit.props && typeof kit.props === 'object' ? kit.props : {}) as Record<
+ string,
+ any
+ >
+ let hireDateField = props.hireDateField ? String(props.hireDateField) : undefined
+ let lastWorkDayField = props.lastWorkDayField ? String(props.lastWorkDayField) : undefined
+ 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
+ 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 }
+}
+
+export function listResignationKitBindings(schema: unknown): ResignationKitBinding[] {
+ const flat: Record[] = []
+ walk(asRules(schema), flat)
+ return flat
+ .filter((r) => String(r.type || '') === 'resignationKit')
+ .map(resolveBinding)
+}
+
+function isEmpty(raw: unknown): boolean {
+ return raw == null || raw === ''
+}
+
+/** 仅对空入职日期写入申请人 hireDate */
+export function applyResignationKitDefaults(
+ schema: unknown,
+ formData: Record,
+ user: Record | null | undefined
+): Record {
+ 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
+}
diff --git a/web-admin/src/views/oa-approval/start/index.vue b/web-admin/src/views/oa-approval/start/index.vue
index 6b106c8c..63291644 100644
--- a/web-admin/src/views/oa-approval/start/index.vue
+++ b/web-admin/src/views/oa-approval/start/index.vue
@@ -171,6 +171,8 @@
listLeaveKitBindings,
markLeaveDurationManual
} from '@/utils/oa-leave-kit-runtime'
+ import { ensureOvertimeKitRuleProps } from '@/utils/oa-overtime-kit-runtime'
+ import { applyResignationKitDefaults } from '@/utils/oa-resignation-kit-runtime'
import {
applyConfirmationKitDateLinkage,
applyConfirmationKitDefaults,
@@ -350,7 +352,9 @@
: []
formRule.value = ensureConfirmationKitVisibility(
ensureOwnerFieldRuleProps(
- ensureLeaveKitRuleProps(ensureOaUploadRuleProps(rawRule))
+ ensureOvertimeKitRuleProps(
+ ensureLeaveKitRuleProps(ensureOaUploadRuleProps(rawRule))
+ )
)
)
const savedGutter = Number(schema.option?.row?.gutter)
@@ -378,6 +382,11 @@
data,
(userStore.info || userStore.getUserInfo) as any
)
+ data = applyResignationKitDefaults(
+ { rule: formRule.value },
+ data,
+ (userStore.info || userStore.getUserInfo) as any
+ )
const linked = applyConfirmationKitDateLinkage({ rule: formRule.value }, data)
formData.value = linked.data
confirmationKitPrevSnapshot = { ...linked.data }
diff --git a/web-admin/src/views/system/user/index.vue b/web-admin/src/views/system/user/index.vue
index a9e22c83..28903a8c 100644
--- a/web-admin/src/views/system/user/index.vue
+++ b/web-admin/src/views/system/user/index.vue
@@ -334,6 +334,16 @@
style="width: 100%"
/>
+
+
+
@@ -502,6 +512,7 @@
phoneNo: '',
hireDate: '' as string,
probationMonths: null as number | null,
+ lastWorkDay: '' as string,
sex: '0',
avatar: '',
status: '1',
@@ -609,6 +620,7 @@
formData.phoneNo = ''
formData.hireDate = ''
formData.probationMonths = null
+ formData.lastWorkDay = ''
formData.sex = '0'
formData.avatar = ''
formData.status = '1'
@@ -648,6 +660,7 @@
detail.probationMonths === null || detail.probationMonths === undefined
? null
: Number(detail.probationMonths)
+ formData.lastWorkDay = detail.lastWorkDay || ''
formData.sex = detail.sex || '0'
formData.avatar = detail.avatar || ''
formData.status = detail.status || '1'
@@ -683,6 +696,7 @@
phoneNo: formData.phoneNo?.trim() || undefined,
hireDate: formData.hireDate || null,
probationMonths: formData.probationMonths,
+ lastWorkDay: formData.lastWorkDay || null,
sex: formData.sex,
status: formData.status,
orderNum: formData.orderNum,
@@ -700,6 +714,7 @@
phoneNo: formData.phoneNo?.trim() || undefined,
hireDate: formData.hireDate || undefined,
probationMonths: formData.probationMonths ?? undefined,
+ lastWorkDay: formData.lastWorkDay || undefined,
sex: formData.sex,
status: formData.status,
orderNum: formData.orderNum,
@@ -811,6 +826,7 @@
? '—'
: `${row.probationMonths} 个月`
},
+ { label: '最后工作日', value: row.lastWorkDay || '—' },
{
label: '性别',
value: