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: