Browse Source

录用审批详情

ZGC4846 6 days ago
parent
commit
672a65458b

+ 277 - 106
src/api/hr/hiringApproval.js

@@ -21,147 +21,318 @@ function dateOnly(value) {
   return String(value).slice(0, 10);
 }
 
-/** 录用审批状态码 → 中文 */
+function toNumber(value) {
+  if (value === null || value === undefined || value === "") return undefined;
+  if (typeof value === "number" && Number.isFinite(value)) return value;
+  const matched = String(value).replace(/,/g, "").match(/(\d+(?:\.\d+)?)/);
+  return matched ? Number(matched[1]) : undefined;
+}
+
+function toProbationMonths(value) {
+  if (value === null || value === undefined || value === "") return undefined;
+  if (typeof value === "number" && Number.isFinite(value)) return value;
+  const matched = String(value).match(/(\d+)/);
+  return matched ? Number(matched[1]) : undefined;
+}
+
+/** 大整数主键保持字符串,避免雪花 ID 精度丢失 */
+function keepBigIntId(value) {
+  if (value === undefined || value === null || value === "") return undefined;
+  const text = String(value).trim();
+  if (!/^-?\d+$/.test(text)) return value;
+  if (!Number.isSafeInteger(Number(text))) return text;
+  return Number(text);
+}
+
+/**
+ * 录用审批状态(文档:PENDING / APPROVED / REJECTED)
+ * 未发起流程、无状态码时前端按草稿展示
+ */
 export const HIRING_STATUS_LABELS = {
   DRAFT: "草稿",
-  PENDING_MY_APPROVAL: "待我审批",
-  APPROVING: "审批中",
+  PENDING: "审批中",
   APPROVED: "已通过",
   REJECTED: "已拒绝",
 };
 
+export const HIRING_STATUS_OPTIONS = Object.entries(HIRING_STATUS_LABELS).map(
+  ([value, label]) => ({ value, label }),
+);
+
 /** 已通过:可发起 Offer */
-export const HIRING_APPROVED_STATUS = new Set(["APPROVED", "已通过"]);
+export const HIRING_APPROVED_STATUS = new Set(["APPROVED"]);
+
+/** BPM 流程标识,需与流程定义 code 一致;提交审核弹框按此拉取流程分类/发起流程 */
+export const HIRING_APPROVAL_BUSINESS_KEY = "hr_recruit_hiring_approval";
 
 export function normalizeHiringStatus(raw = {}) {
-  const code = String(
-    raw.approvalStatus || raw.status || raw.statusCode || "",
-  ).toUpperCase();
-  const labelMap = {
-    DRAFT: "DRAFT",
-    草稿: "DRAFT",
-    PENDING_MY_APPROVAL: "PENDING_MY_APPROVAL",
-    WAITING: "PENDING_MY_APPROVAL",
-    待我审批: "PENDING_MY_APPROVAL",
-    APPROVING: "APPROVING",
-    PENDING_APPROVAL: "APPROVING",
-    审批中: "APPROVING",
-    APPROVED: "APPROVED",
-    PASSED: "APPROVED",
-    已通过: "APPROVED",
-    REJECTED: "REJECTED",
-    已拒绝: "REJECTED",
-    已驳回: "REJECTED",
-  };
-  return labelMap[code] || labelMap[raw.approvalStatus] || code || "DRAFT";
+  const code = String(raw.approvalStatus || raw.status || "").trim().toUpperCase();
+  if (code === "APPROVED" || code === "PASSED") return "APPROVED";
+  if (code === "REJECTED" || code === "已拒绝" || code === "已驳回") return "REJECTED";
+  if (
+    code === "PENDING" ||
+    code === "APPROVING" ||
+    code === "PENDING_APPROVAL" ||
+    code === "WAITING"
+  ) {
+    return "PENDING";
+  }
+  if (raw.processInstanceId) return "PENDING";
+  if (raw.id) return "DRAFT";
+  return "DRAFT";
 }
 
-export function adaptHiringApproval(raw = {}) {
+/** 适配「录用审批视图」+ 前端展示扩展字段 */
+export function adaptHiringApproval(raw = {}, extras = {}) {
   const statusCode = normalizeHiringStatus(raw);
   const hireDate =
-    dateOnly(raw.plannedHireDate || raw.hireDate || raw.onboardingDate) || "";
-  const experienceRaw = raw.experience ?? raw.workYears ?? "";
+    dateOnly(raw.expectedEntryDate || extras.expectedEntryDate || raw.hireDate) ||
+    "";
+  const departmentId =
+    extras.departmentId ||
+    raw.hiringDeptId ||
+    raw.departmentId ||
+    raw.deptId;
+  const departmentName =
+    extras.departmentName ||
+    raw.hiringDeptName ||
+    raw.departmentName ||
+    raw.deptName ||
+    "";
+  const phone =
+    extras.phone ||
+    extras.mobile ||
+    raw.contactMobile ||
+    raw.phone ||
+    raw.mobile ||
+    "";
   return {
     ...raw,
     id: raw.id,
-    approvalNo: raw.approvalNo || (raw.id ? `HA${raw.id}` : ""),
-    candidateId: raw.candidateId,
-    candidateName: raw.candidateName || raw.name || "",
-    gender: raw.gender || raw.title || "",
-    education: raw.education || "",
-    phone: raw.phone || raw.mobile || raw.candidatePhone || "",
-    experience: experienceRaw,
-    positionId: raw.positionId,
-    positionName: raw.positionName || raw.appliedPosition || "",
-    departmentId: raw.departmentId || raw.recruitmentDeptId || raw.deptId,
-    departmentName:
-      raw.departmentName ||
-      raw.recruitmentDeptName ||
-      raw.deptName ||
-      raw.department ||
-      "",
-    proposedSalary: raw.proposedSalary || raw.salaryAdvice || raw.salary || "",
-    probation: raw.probation || raw.probationPeriod || "",
-    salaryStructure: raw.salaryStructure || "",
-    staffingType:
+    approvalNo:
+      raw.approvalNo ||
+      raw.documentCode ||
+      (raw.id != null ? `HA${raw.id}` : ""),
+    documentCode: raw.documentCode || "",
+    applicationId: raw.applicationId ?? extras.applicationId,
+    candidateId:
+      raw.candidateId ??
+      extras.candidateId ??
+      raw.userId ??
+      extras.userId,
+    userId: raw.userId ?? extras.userId ?? raw.candidateId,
+    candidateName:
+      extras.candidateName || raw.candidateName || raw.name || "",
+    positionId: raw.positionId ?? extras.positionId,
+    positionName: raw.positionName || extras.positionName || "",
+    hiringDeptId: raw.hiringDeptId ?? departmentId,
+    hiringDeptName: raw.hiringDeptName || departmentName,
+    departmentId,
+    departmentName,
+    gender: extras.gender || raw.gender || "",
+    education: extras.education ?? raw.education ?? "",
+    phone,
+    mobile: phone,
+    contactMobile: raw.contactMobile || phone,
+    proposedSalary: raw.proposedSalary ?? extras.proposedSalary,
+    workYears:
+      extras.workYears ??
+      raw.workYears ??
+      extras.experienceYears ??
+      raw.experienceYears,
+    employmentType:
+      extras.employmentType ||
+      raw.employmentType ||
       raw.staffingType ||
-      raw.positionNature ||
-      raw.positionType ||
-      raw.编制类型 ||
       "",
+    staffingType: extras.employmentType || raw.employmentType || raw.staffingType || "",
+    probationPeriod: raw.probationPeriod ?? extras.probationPeriod,
+    probation:
+      (raw.probationPeriod ?? extras.probationPeriod) != null &&
+      (raw.probationPeriod ?? extras.probationPeriod) !== ""
+        ? `${raw.probationPeriod ?? extras.probationPeriod}个月`
+        : extras.probation || raw.probation || "",
     hireDate,
-    currentNode: raw.currentNode || raw.nodeName || raw.approvalNode || "-",
+    expectedEntryDate: hireDate,
+    workLocation: extras.workLocation || raw.workLocation || "",
+    specialTerms: extras.specialTerms || raw.specialTerms || raw.remark || "",
+    remark: extras.specialTerms || raw.specialTerms || raw.remark || "",
+    specialApprovalReason: raw.specialApprovalReason || "",
+    processInstanceId: raw.processInstanceId || "",
+    currentNode: extras.currentNode || raw.currentNode || raw.nodeName || "-",
+    resumeAttachmentFileIds:
+      raw.resumeAttachmentFileIds || extras.resumeAttachmentFileIds || "",
     statusCode,
     status: HIRING_STATUS_LABELS[statusCode] || raw.approvalStatus || "-",
-    remark: raw.remark || raw.approvalComment || "",
-    attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
-    applicationId: raw.applicationId,
     offerId: raw.offerId,
-    offerSent: Boolean(raw.offerId || raw.offerSent),
+    offerSent: Boolean(raw.offerId || extras.offerSent),
+    createTime: raw.createTime,
+    createUserId: raw.createUserId,
   };
 }
 
+function toEducationValue(value) {
+  if (value === null || value === undefined || value === "") return undefined;
+  const text = String(value).trim();
+  return text || undefined;
+}
+
+export function parseResumeAttachmentFileIds(value) {
+  if (Array.isArray(value)) {
+    return value
+      .map((item) => String(item ?? "").trim())
+      .filter(Boolean);
+  }
+  if (value == null || value === "") return [];
+  return String(value)
+    .split(",")
+    .map((item) => item.trim())
+    .filter(Boolean);
+}
+
+function joinResumeFileIds(form = {}) {
+  const files = Array.isArray(form.attachmentFiles)
+    ? form.attachmentFiles
+    : Array.isArray(form.attachments)
+      ? form.attachments
+      : null;
+  if (files) {
+    const ids = files
+      .map((item) => keepBigIntId(item.id || item.fileId))
+      .filter((id) => id != null && id !== "");
+    return ids.length ? ids.join(",") : undefined;
+  }
+  if (form.resumeAttachmentFileIds) {
+    return (
+      parseResumeAttachmentFileIds(form.resumeAttachmentFileIds).join(",") ||
+      undefined
+    );
+  }
+  return undefined;
+}
+
+/**
+ * 保存草稿请求体 → 录用审批新增参数
+ * 必填:expectedEntryDate、proposedSalary
+ * 线下(无 applicationId):name、positionId 必填
+ */
 export function serializeHiringApproval(form = {}) {
-  const experienceYears =
-    form.experience === null ||
-    form.experience === undefined ||
-    form.experience === ""
-      ? undefined
-      : Number(form.experience);
-  const experience =
-    experienceYears == null || Number.isNaN(experienceYears)
-      ? undefined
-      : `${experienceYears}年`;
-  return compact({
-    id: form.id || undefined,
-    candidateId: form.candidateId || undefined,
-    candidateName: form.candidateName || form.name,
+  const applicationId = keepBigIntId(form.applicationId);
+  const name = String(form.name || form.candidateName || "").trim();
+  const employmentType = keepBigIntId(form.employmentType ?? form.staffingType);
+  const candidateId = keepBigIntId(
+    form.candidateId ?? form.userId ?? form.personId,
+  );
+  const payload = compact({
+    id: keepBigIntId(form.id),
+    applicationId,
+    candidateId,
+    contactMobile: String(
+      form.contactMobile || form.phone || form.mobile || "",
+    ).trim() || undefined,
+    demandId: keepBigIntId(form.demandId),
+    education: toEducationValue(form.education),
+    employmentType,
+    expectedEntryDate: dateOnly(form.expectedEntryDate || form.hireDate),
     gender: form.gender || undefined,
-    education: form.education || undefined,
-    phone: form.phone || undefined,
-    experience,
-    workYears: experienceYears,
-    positionId: form.positionId || undefined,
-    positionName: form.positionName || undefined,
-    departmentId: form.departmentId || undefined,
-    departmentName: form.departmentName || undefined,
-    proposedSalary: form.proposedSalary || undefined,
-    probation: form.probation || undefined,
-    plannedHireDate: dateOnly(form.hireDate),
-    salaryStructure: form.salaryStructure || undefined,
-    staffingType: form.staffingType || undefined,
-    remark: form.remark || undefined,
-    applicationId: form.applicationId || undefined,
-    attachmentFileIds: Array.isArray(form.attachmentFileIds)
-      ? form.attachmentFileIds
-      : undefined,
+    name: name || undefined,
+    positionId: keepBigIntId(form.positionId),
+    probationPeriod: toProbationMonths(form.probationPeriod ?? form.probation),
+    proposedLevel: form.proposedLevel || undefined,
+    proposedSalary: (() => {
+      const amount = toNumber(form.proposedSalary);
+      return amount == null ? undefined : Math.round(amount);
+    })(),
+    resumeAttachmentFileIds: joinResumeFileIds(form),
+    workYears: (() => {
+      const years = toNumber(form.workYears);
+      return years == null ? undefined : Math.round(years);
+    })(),
+  });
+  if (payload.applicationId == null || payload.applicationId === "") {
+    delete payload.applicationId;
+  }
+  payload.workLocation = String(form.workLocation || "").trim();
+  payload.specialTerms = String(form.specialTerms || form.remark || "").trim();
+  return payload;
+}
+
+/** 校验保存草稿必填(文档:expectedEntryDate、proposedSalary;线下须 name、positionId) */
+export function validateOfflineHiringDraft(form = {}) {
+  const errors = [];
+  if (!dateOnly(form.expectedEntryDate || form.hireDate)) {
+    errors.push("请选择预计入职日期");
+  }
+  if (toNumber(form.proposedSalary) == null) {
+    errors.push("请输入拟定薪资");
+  }
+  if (
+    !keepBigIntId(form.applicationId) &&
+    !String(form.name || form.candidateName || "").trim()
+  ) {
+    errors.push("请填写姓名");
+  }
+  if (!keepBigIntId(form.applicationId) && !keepBigIntId(form.positionId)) {
+    errors.push("请选择应聘岗位");
+  }
+  return errors;
+}
+
+function toQueryInt(value) {
+  if (value === null || value === undefined || value === "") return undefined;
+  if (typeof value === "number" && Number.isFinite(value)) return value;
+  const text = String(value).trim();
+  if (!/^-?\d+$/.test(text)) return undefined;
+  return keepBigIntId(text);
+}
+
+/**
+ * 分页查询参数(GET /recruit/hiring-approvals)
+ * approvalStatus / deptId / keyword / offset / orderBy / pageNum / size / sortName
+ */
+export function serializeHiringApprovalQuery(params = {}) {
+  return compact({
+    approvalStatus: params.approvalStatus || undefined,
+    deptId: toQueryInt(params.deptId),
+    keyword: params.keyword || undefined,
+    offset: toQueryInt(params.offset),
+    orderBy: params.orderBy || undefined,
+    pageNum: toQueryInt(params.pageNum),
+    size: toQueryInt(params.size),
+    sortName: params.sortName || undefined,
   });
 }
 
+// —— 人力资源-录用审批管理 ——
+
+/** 录用审批分页查询 GET /recruit/hiring-approvals */
 export const getHiringApprovalPage = async (params = {}) =>
   unwrap(
-    await request.get("/recruit/hiring-approvals", { params: compact(params) }),
+    await request.get("/recruit/hiring-approvals", {
+      params: serializeHiringApprovalQuery(params),
+    }),
   );
 
-export const getHiringApprovalById = async (id) =>
-  adaptHiringApproval(
-    await unwrap(await request.get(`/recruit/hiring-approvals/${id}`)),
+/** 录用审批详情查询 GET /recruit/hiring-approvals/{id},id 为列表主键 */
+export const getHiringApprovalById = async (id) => {
+  const hiringId = keepBigIntId(id);
+  if (hiringId == null || hiringId === "") {
+    return Promise.reject(new Error("缺少录用审批主键"));
+  }
+  return adaptHiringApproval(
+    await unwrap(await request.get(`/recruit/hiring-approvals/${hiringId}`)),
   );
+};
 
-export const createHiringApproval = async (data) =>
-  unwrap(await request.post("/recruit/hiring-approvals", data));
+/**
+ * 保存录用审批草稿 POST /recruit/hiring-approvals
+ * 成功返回主键 id;前端发起时再调 BPM create
+ */
+export const createHiringApproval = async (data) => {
+  const payload = serializeHiringApproval(data || {});
+  return unwrap(await request.post("/recruit/hiring-approvals", payload));
+};
 
+/** 与 create 同一接口(文档无独立修改接口) */
 export const updateHiringApproval = async (id, data) =>
-  unwrap(await request.put(`/recruit/hiring-approvals/${id}`, data));
-
-export const submitHiringApproval = async (id, data = {}) =>
-  unwrap(await request.post(`/recruit/hiring-approvals/${id}/submit`, data));
-
-export const approveHiringApproval = async (id, data = {}) =>
-  unwrap(await request.post(`/recruit/hiring-approvals/${id}/approve`, data));
-
-export const rejectHiringApproval = async (id, data = {}) =>
-  unwrap(await request.post(`/recruit/hiring-approvals/${id}/reject`, data));
-
-export const deleteHiringApproval = async (id) =>
-  unwrap(await request.delete(`/recruit/hiring-approvals/${id}`));
+  createHiringApproval({ ...(data || {}), id: id ?? data?.id });

+ 239 - 0
src/styles/views/hiringApproval/detail.scss

@@ -0,0 +1,239 @@
+.hiring-approval-page {
+  min-height: 100%;
+  padding: 16px 18px 22px;
+  color: #203047;
+  background: #f5f7fb;
+}
+
+.detail-page-header {
+  margin-bottom: 12px;
+  padding: 16px 18px;
+  border: 1px solid #eef2f7;
+  border-radius: 12px;
+  background: #fff;
+}
+
+.dialog-title-bar {
+  min-width: 0;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+}
+
+.dialog-title-text h2 {
+  margin: 0;
+  color: #1f2e43;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.dialog-title-text p {
+  margin: 4px 0 0;
+  color: #8b97a7;
+  font-size: 12px;
+}
+
+.dialog-title-bar > em {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 5px 9px;
+  border-radius: 12px;
+  font-size: 12px;
+  font-style: normal;
+  white-space: nowrap;
+}
+
+.dialog-title-bar > em i {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: currentColor;
+}
+
+.dialog-title-bar > em.is-success {
+  color: #118969;
+  background: #e8f8f3;
+}
+
+.dialog-title-bar > em.is-danger {
+  color: #d45454;
+  background: #ffecec;
+}
+
+.dialog-title-bar > em.is-primary {
+  color: #1768e5;
+  background: #eaf2ff;
+}
+
+.dialog-title-bar > em.is-warning {
+  color: #c46f11;
+  background: #fff4e5;
+}
+
+.page-body {
+  min-width: 0;
+}
+
+.special-alert {
+  margin-bottom: 12px;
+}
+
+.info-cards {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 8px;
+}
+
+.info-card,
+.attach-card {
+  padding: 16px 18px 14px;
+  border: 1px solid #e3eaf4;
+  border-radius: 12px;
+  background: #fff;
+  box-shadow: 0 6px 18px rgba(30, 64, 110, 0.05);
+}
+
+.attach-card {
+  margin-top: 8px;
+}
+
+.info-card-head {
+  margin-bottom: 14px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 8px;
+}
+
+.info-card-head h3 {
+  margin: 0;
+  color: #20304a;
+  font-size: 15px;
+  font-weight: 600;
+}
+
+.info-card-head i {
+  color: #9aa8bb;
+  font-size: 13px;
+}
+
+.info-card .el-row {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  column-gap: 16px;
+  row-gap: 0;
+}
+
+.info-card .el-row::before,
+.info-card .el-row::after {
+  display: none;
+}
+
+.info-card .el-col,
+.info-card [class*='el-col-'] {
+  float: none;
+  width: 100%;
+  max-width: none;
+  padding-left: 0;
+  padding-right: 0;
+}
+
+.info-card .el-col.is-full,
+.info-card .el-col-24 {
+  grid-column: 1 / -1;
+}
+
+.hiring-form ::v-deep .el-form-item {
+  margin-bottom: 0;
+}
+
+.hiring-form ::v-deep .el-form-item__content {
+  padding-bottom: 6px;
+}
+
+.hiring-form ::v-deep .el-form-item__label {
+  padding-bottom: 6px;
+  color: #526177;
+  font-size: 13px;
+  font-weight: 600;
+  line-height: 20px;
+}
+
+.hiring-form ::v-deep .el-input__inner {
+  height: 40px;
+  color: #34435a;
+  border-color: #dce4ef;
+  border-radius: 8px;
+}
+
+.hiring-form ::v-deep .el-select,
+.hiring-form ::v-deep .el-input-number,
+.hiring-form ::v-deep .el-date-editor,
+.full-width {
+  width: 100%;
+}
+
+.unit-field {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.unit-field .full-width {
+  flex: 1;
+}
+
+.unit-suffix {
+  flex-shrink: 0;
+  color: #64748b;
+  font-size: 13px;
+}
+
+.field-hint {
+  margin: 0;
+  color: #94a3b8;
+  font-size: 12px;
+  line-height: 1.5;
+}
+
+.attach-row {
+  display: flex;
+  align-items: flex-start;
+  gap: 16px;
+}
+
+.attach-row > label {
+  width: 88px;
+  flex: 0 0 88px;
+  padding-top: 6px;
+  color: #526177;
+  font-size: 13px;
+  font-weight: 600;
+}
+
+.attach-content {
+  min-width: 0;
+  flex: 1;
+}
+
+@media (max-width: 760px) {
+  .hiring-approval-page {
+    padding: 12px;
+  }
+
+  .info-cards {
+    grid-template-columns: 1fr;
+  }
+
+  .attach-row {
+    flex-direction: column;
+    gap: 8px;
+  }
+
+  .attach-row > label {
+    width: 100%;
+    flex-basis: auto;
+  }
+}

+ 14 - 1
src/views/bpm/done/detailDialog.vue

@@ -254,7 +254,9 @@ import businessTripComponent from '@/BIZComponents/processSubmitDialog/component
         if (this.isStockBatchChange() && (!router || invalidViewRouter)) {
           return '/bpm/handleTask/components/stockBatchChange/detailDialog.vue';
         }
-        return router;
+        const path = String(router || '').trim();
+        if (!path || path === '/' || !path.startsWith('/')) return '';
+        return path;
       },
       async open(row) {
         this.form = _.cloneDeep(row);
@@ -281,6 +283,17 @@ import businessTripComponent from '@/BIZComponents/processSubmitDialog/component
         this.visible = true;
         console.log('this.form.pcViewRouter', this.form.pcViewRouter);
         const router = this.getBusinessComponentRouter();
+        if (!router) {
+          console.error('流程表单路由未配置', {
+            pcViewRouter: this.form.pcViewRouter,
+            businessCode: this.form.businessCode,
+            businessType: this.form.businessType
+          });
+          this.$message.error(
+            '流程未配置 PC 详情路由(pcViewRouter),请在流程定义中填写业务详情组件路径'
+          );
+          return;
+        }
         Vue.component('async-biz-form-component', (resolve) => {
           require([`@/views${router}`], resolve);
         });

+ 578 - 0
src/views/bpm/handleTask/components/hiringApproval/hiringApprovalDetail.vue

@@ -0,0 +1,578 @@
+<template>
+  <main class="hiring-approval-page" v-loading="loading">
+    <header class="detail-page-header">
+      <div class="dialog-title-bar">
+        <div class="dialog-title-text">
+          <h2>录用审批详情</h2>
+          <p v-if="form.approvalNo">{{ form.approvalNo }}</p>
+        </div>
+        <em v-if="form.status" :class="statusTone(form.statusCode)"
+          ><i></i>{{ form.status }}</em
+        >
+      </div>
+    </header>
+
+    <div class="page-body">
+      <el-alert
+        v-if="form.specialApprovalReason"
+        type="warning"
+        :closable="false"
+        show-icon
+        class="special-alert"
+        :title="`特殊审批:${form.specialApprovalReason}`"
+      />
+
+      <el-form
+        ref="form"
+        :model="form"
+        label-position="top"
+        class="hiring-form"
+        disabled
+        @submit.native.prevent
+      >
+        <div class="info-cards">
+          <section class="info-card">
+            <div class="info-card-head">
+              <h3>候选人信息</h3>
+              <i class="el-icon-arrow-down"></i>
+            </div>
+            <el-row :gutter="16">
+              <el-col :span="12">
+                <el-form-item label="姓名" prop="candidateName">
+                  <el-input
+                    v-model.trim="form.candidateName"
+                    class="full-width"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="联系方式" prop="phone">
+                  <el-input v-model.trim="form.phone" class="full-width" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="性别">
+                  <el-select
+                    v-model="form.gender"
+                    class="full-width"
+                    placeholder="—"
+                  >
+                    <el-option
+                      v-for="item in genderOptions"
+                      :key="item"
+                      :label="item"
+                      :value="item"
+                    />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="学历">
+                  <el-select
+                    v-model="form.education"
+                    class="full-width"
+                    placeholder="—"
+                  >
+                    <el-option
+                      v-for="item in educationOptions"
+                      :key="item.key || item.value"
+                      :label="item.label"
+                      :value="item.value"
+                    />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="入职部门" prop="departmentId">
+                  <el-select
+                    v-model="form.departmentId"
+                    class="full-width"
+                    placeholder="—"
+                  >
+                    <el-option
+                      v-for="item in departmentOptions"
+                      :key="item.id"
+                      :label="item.name"
+                      :value="item.id"
+                    />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="应聘岗位" prop="positionId">
+                  <el-select
+                    v-model="form.positionId"
+                    class="full-width"
+                    placeholder="—"
+                  >
+                    <el-option
+                      v-for="item in positionOptions"
+                      :key="item.id"
+                      :label="positionLabel(item)"
+                      :value="item.id"
+                    />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="工作经验">
+                  <div class="unit-field">
+                    <el-input-number
+                      v-model="form.workYears"
+                      :min="0"
+                      :max="50"
+                      :precision="0"
+                      controls-position="right"
+                      class="full-width"
+                    />
+                    <span class="unit-suffix">年</span>
+                  </div>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <p class="field-hint">先选入职部门,再按部门加载应聘岗位。</p>
+          </section>
+
+          <section class="info-card">
+            <div class="info-card-head">
+              <h3>录用信息</h3>
+            </div>
+            <el-row :gutter="16">
+              <el-col :span="12">
+                <el-form-item label="拟定薪资(元/月)" prop="proposedSalary">
+                  <el-input-number
+                    v-model="form.proposedSalary"
+                    :min="0"
+                    :precision="0"
+                    :step="1000"
+                    controls-position="right"
+                    class="full-width"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="试用期(月)" prop="probationPeriod">
+                  <el-select
+                    v-model="form.probationPeriod"
+                    class="full-width"
+                    placeholder="—"
+                  >
+                    <el-option
+                      v-for="item in probationMonthOptions"
+                      :key="item.value"
+                      :label="item.label"
+                      :value="item.value"
+                    />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="预计入职日期" prop="expectedEntryDate">
+                  <el-date-picker
+                    v-model="form.expectedEntryDate"
+                    type="date"
+                    value-format="yyyy-MM-dd"
+                    class="full-width"
+                    placeholder="—"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="用工类型" prop="employmentType">
+                  <el-select
+                    v-model="form.employmentType"
+                    class="full-width"
+                    placeholder="—"
+                  >
+                    <el-option
+                      v-for="item in positionNatureOptions"
+                      :key="item.id != null ? item.id : item.key || item.value"
+                      :label="item.label"
+                      :value="item.value"
+                    />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="24" class="is-full">
+                <el-form-item label="工作地点">
+                  <el-input
+                    v-model.trim="form.workLocation"
+                    class="full-width"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="24" class="is-full">
+                <el-form-item label="特殊约定">
+                  <el-input
+                    v-model.trim="form.specialTerms"
+                    type="textarea"
+                    :rows="2"
+                    class="full-width"
+                  />
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </section>
+        </div>
+
+        <section class="attach-card">
+          <div class="info-card-head">
+            <h3>附件上传</h3>
+          </div>
+          <div class="attach-row">
+            <label>简历</label>
+            <div class="attach-content">
+              <fileMain v-model="resumeFileIds" type="view" size="small" />
+            </div>
+          </div>
+        </section>
+      </el-form>
+    </div>
+  </main>
+</template>
+
+<script>
+import { getPositionById, getPositionPage } from '@/api/hr';
+import {
+  getHiringApprovalById,
+  parseResumeAttachmentFileIds
+} from '@/api/hr/hiringApproval';
+import { listOrganizations } from '@/api/organization';
+import { getDictByCode } from '@/api/sys';
+
+const emptyForm = () => ({
+  id: null,
+  approvalNo: '',
+  applicationId: null,
+  candidateId: null,
+  candidateName: '',
+  gender: '',
+  phone: '',
+  education: '',
+  workYears: undefined,
+  positionId: null,
+  positionName: '',
+  departmentId: null,
+  departmentName: '',
+  proposedSalary: undefined,
+  expectedEntryDate: '',
+  probationPeriod: 3,
+  employmentType: '',
+  workLocation: '',
+  specialTerms: '',
+  specialApprovalReason: '',
+  processInstanceId: '',
+  status: '',
+  statusCode: '',
+  currentNode: '',
+  hireDate: '',
+  resumeAttachmentFileIds: ''
+});
+
+const probationMonthOptions = [
+  { value: 1, label: '1个月' },
+  { value: 2, label: '2个月' },
+  { value: 3, label: '3个月' },
+  { value: 6, label: '6个月' }
+];
+
+const DEFAULT_GENDER_OPTIONS = ['男', '女'];
+const DEFAULT_EDUCATION_OPTIONS = [
+  { label: '初中', value: '初中', key: '初中' },
+  { label: '高中', value: '高中', key: '高中' },
+  { label: '中专', value: '中专', key: '中专' },
+  { label: '大专', value: '大专', key: '大专' },
+  { label: '本科', value: '本科', key: '本科' },
+  { label: '硕士', value: '硕士', key: '硕士' },
+  { label: '博士', value: '博士', key: '博士' }
+];
+
+function asList(data) {
+  if (Array.isArray(data)) return data;
+  if (Array.isArray(data?.list)) return data.list;
+  if (Array.isArray(data?.records)) return data.records;
+  return [];
+}
+
+export default {
+  name: 'HiringApprovalDetail',
+  props: {
+    businessId: {
+      default: ''
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      positionLoading: false,
+      departmentLoading: false,
+      form: emptyForm(),
+      probationMonthOptions,
+      genderOptions: [...DEFAULT_GENDER_OPTIONS],
+      educationOptions: [...DEFAULT_EDUCATION_OPTIONS],
+      positionNatureOptions: [],
+      positionOptions: [],
+      departmentOptions: [],
+      resumeFileIds: []
+    };
+  },
+  watch: {
+    businessId: {
+      immediate: true,
+      handler(id) {
+        if (id) this.bootstrap(id);
+      }
+    }
+  },
+  methods: {
+    statusTone(code) {
+      if (code === 'APPROVED') return 'is-success';
+      if (code === 'REJECTED') return 'is-danger';
+      if (code === 'PENDING') return 'is-primary';
+      return 'is-warning';
+    },
+    positionLabel(item = {}) {
+      return item.name || item.positionName || '-';
+    },
+    async bootstrap(id) {
+      this.form = emptyForm();
+      this.resumeFileIds = [];
+      this.positionOptions = [];
+      this.loading = true;
+      try {
+        await Promise.all([
+          this.loadPositionNatureOptions(),
+          this.loadDictOptions(),
+          this.loadDepartments()
+        ]);
+        const detail = await getHiringApprovalById(id);
+        this.applyDetail(detail);
+        await this.fillRelatedInfo();
+      } catch (error) {
+        this.form = emptyForm();
+        this.$message.error(error?.message || '录用审批详情加载失败');
+      } finally {
+        this.loading = false;
+      }
+    },
+    applyDetail(detail = {}) {
+      const expectedEntryDate =
+        detail.expectedEntryDate || detail.hireDate || '';
+      const workYears =
+        detail.workYears === '' || detail.workYears == null
+          ? undefined
+          : Number(detail.workYears);
+      this.form = {
+        ...emptyForm(),
+        ...detail,
+        applicationId: detail.applicationId ?? null,
+        candidateId:
+          detail.candidateId ?? detail.userId ?? detail.personId ?? null,
+        candidateName: detail.candidateName || detail.name || '',
+        gender: detail.gender || '',
+        phone: detail.phone || detail.contactMobile || detail.mobile || '',
+        education: this.resolveEducation(detail.education || ''),
+        workYears: Number.isFinite(workYears)
+          ? Math.round(workYears)
+          : undefined,
+        positionId: detail.positionId ?? null,
+        positionName: detail.positionName || '',
+        departmentId:
+          detail.departmentId ??
+          detail.hiringDeptId ??
+          detail.deptId ??
+          null,
+        departmentName:
+          detail.departmentName ||
+          detail.hiringDeptName ||
+          detail.deptName ||
+          '',
+        resumeAttachmentFileIds: detail.resumeAttachmentFileIds || '',
+        proposedSalary:
+          detail.proposedSalary === '' || detail.proposedSalary == null
+            ? undefined
+            : Math.round(Number(detail.proposedSalary)),
+        expectedEntryDate,
+        hireDate: expectedEntryDate,
+        probationPeriod:
+          detail.probationPeriod == null || detail.probationPeriod === ''
+            ? 3
+            : Number(detail.probationPeriod),
+        employmentType: this.resolveEmploymentType(
+          detail.employmentType || detail.staffingType || ''
+        ),
+        workLocation: detail.workLocation || '',
+        specialTerms: detail.specialTerms || detail.remark || '',
+        specialApprovalReason: detail.specialApprovalReason || ''
+      };
+      this.resumeFileIds = parseResumeAttachmentFileIds(
+        detail.resumeAttachmentFileIds
+      );
+    },
+    async loadDictOptions() {
+      try {
+        const [genderItems, educationItems] = await Promise.all([
+          getDictByCode('gender').catch(() => []),
+          getDictByCode('education').catch(() =>
+            getDictByCode('education_background').catch(() => [])
+          )
+        ]);
+        const genders = (genderItems || [])
+          .map((item) => String(item.value ?? item.label ?? '').trim())
+          .filter(Boolean);
+        const educations = (educationItems || [])
+          .map((item) => {
+            const label = String(item.label ?? item.value ?? '').trim();
+            const key =
+              item.key != null && String(item.key).trim() !== ''
+                ? String(item.key)
+                : '';
+            if (!label || !key) return null;
+            return { label, value: key, key };
+          })
+          .filter(Boolean);
+        if (genders.length) this.genderOptions = genders;
+        if (educations.length) this.educationOptions = educations;
+        this.form.education = this.resolveEducation(this.form.education);
+      } catch (e) {
+        /* 使用默认选项 */
+      }
+    },
+    async loadPositionNatureOptions() {
+      try {
+        const items = await getDictByCode('position_nature');
+        this.positionNatureOptions = (items || [])
+          .map((item) => {
+            const label = String(item.label ?? item.value ?? '').trim();
+            const id = item.id != null && item.id !== '' ? item.id : item.key;
+            if (!label || id == null || id === '') return null;
+            return {
+              label,
+              value: id,
+              key: item.key != null ? String(item.key) : String(id),
+              id
+            };
+          })
+          .filter(Boolean);
+        this.form.employmentType = this.resolveEmploymentType(
+          this.form.employmentType
+        );
+      } catch (error) {
+        this.positionNatureOptions = [];
+      }
+    },
+    resolveEducation(raw) {
+      if (raw == null || raw === '') return '';
+      const text = String(raw);
+      const hit = this.educationOptions.find(
+        (item) =>
+          String(item.value) === text ||
+          String(item.key) === text ||
+          String(item.label) === text
+      );
+      return hit ? hit.value : raw;
+    },
+    resolveEmploymentType(raw) {
+      if (raw == null || raw === '') return '';
+      const text = String(raw);
+      const hit = this.positionNatureOptions.find(
+        (item) =>
+          String(item.value) === text ||
+          String(item.id) === text ||
+          String(item.key) === text ||
+          String(item.label) === text
+      );
+      return hit ? hit.value : raw;
+    },
+    async loadPositions(deptId) {
+      if (deptId == null || deptId === '') {
+        this.positionOptions = [];
+        return;
+      }
+      this.positionLoading = true;
+      try {
+        const data = await getPositionPage({
+          pageNum: 1,
+          size: 500,
+          deptId
+        });
+        this.positionOptions = asList(data).map((item) => ({
+          id: item.id,
+          name: item.positionName || item.name || '',
+          deptId: item.deptId ?? deptId,
+          deptName: item.deptName || ''
+        }));
+      } catch (error) {
+        this.positionOptions = [];
+      } finally {
+        this.positionLoading = false;
+      }
+    },
+    async loadDepartments() {
+      this.departmentLoading = true;
+      try {
+        const list = await listOrganizations();
+        const records = Array.isArray(list) ? list : asList(list);
+        this.departmentOptions = records
+          .map((item) => ({
+            id: item.id,
+            name: item.name || item.orgName || item.groupName || ''
+          }))
+          .filter((item) => item.id != null && item.name);
+      } catch (error) {
+        this.departmentOptions = [];
+      } finally {
+        this.departmentLoading = false;
+      }
+    },
+    async fillRelatedInfo() {
+      if (
+        this.form.positionId != null &&
+        (this.form.departmentId == null || this.form.departmentId === '')
+      ) {
+        try {
+          const pos = await getPositionById(this.form.positionId);
+          if (pos?.deptId != null) {
+            this.form.departmentId = pos.deptId;
+            this.form.departmentName =
+              pos.deptName || this.form.departmentName || '';
+            if (!this.form.positionName) {
+              this.form.positionName = pos.name || pos.positionName || '';
+            }
+          }
+        } catch (e) {
+          /* ignore */
+        }
+      }
+      if (this.form.departmentId != null && !this.form.departmentName) {
+        this.form.departmentName =
+          this.departmentOptions.find(
+            (item) => String(item.id) === String(this.form.departmentId)
+          )?.name || '';
+      }
+      if (this.form.departmentId != null && this.form.departmentId !== '') {
+        const keepPositionId = this.form.positionId;
+        const keepPositionName = this.form.positionName;
+        await this.loadPositions(this.form.departmentId);
+        if (
+          keepPositionId != null &&
+          !this.positionOptions.some(
+            (item) => String(item.id) === String(keepPositionId)
+          )
+        ) {
+          this.positionOptions.unshift({
+            id: keepPositionId,
+            name: keepPositionName || `岗位-${keepPositionId}`,
+            deptId: this.form.departmentId,
+            deptName: this.form.departmentName
+          });
+        }
+        this.form.positionId = keepPositionId;
+        this.form.positionName = keepPositionName;
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped src="@/styles/views/hiringApproval/detail.scss"></style>

+ 125 - 0
src/views/bpm/handleTask/components/hiringApproval/submit.vue

@@ -0,0 +1,125 @@
+<template>
+  <el-col :span="16" :offset="6">
+    <el-form label-width="100px" ref="formRef" :model="form">
+      <el-form-item
+        label="审批建议"
+        style="margin-bottom: 20px"
+        :rules="{
+          required: true,
+          message: '请选择',
+          trigger: 'change'
+        }"
+      >
+        <el-input
+          type="textarea"
+          v-model="form.reason"
+          placeholder="请输入审批建议"
+        />
+      </el-form-item>
+    </el-form>
+
+    <div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
+      <el-button
+        icon="el-icon-edit-outline"
+        type="success"
+        size="mini"
+        :loading="isLoading"
+        v-click-once
+        @click="handleAudit(1)"
+        >通过
+      </el-button>
+
+      <el-button
+        icon="el-icon-circle-close"
+        type="danger"
+        size="mini"
+        v-click-once
+        :loading="isLoading"
+        @click="outApproveNotPass(0)"
+        >驳回
+      </el-button>
+    </div>
+  </el-col>
+</template>
+<script>
+  import { approveTaskWithVariables, outApproveNotPass } from '@/api/bpm/task';
+  export default {
+    data() {
+      return {
+        form: {
+          reason: '同意'
+        },
+        isLoading: false
+      };
+    },
+    props: {
+      businessId: {
+        default: ''
+      },
+      taskId: {
+        default: ''
+      },
+      id: {
+        default: ''
+      },
+
+      taskDefinitionKey: {
+        default: ''
+      }
+    },
+    methods: {
+      async outApproveNotPass(status) {
+        const params = {
+          id: this.taskId,
+          reason: this.form.reason,
+          outInId: this.businessId
+        };
+        try {
+          this.isLoading = true;
+          const data = await outApproveNotPass(params);
+          if (data.data.code != '-1') {
+            this.$emit('handleAudit', {
+              status,
+              title: '驳回'
+            });
+          }
+          this.isLoading = false;
+        } catch (error) {
+          this.isLoading = false;
+        }
+      },
+
+      handleAudit(status) {
+        if (!this.form.reason && status == 1) {
+          this.$message.warning(`请填写审批意见!`);
+          return;
+        }
+
+        this._approveTaskWithVariables(status);
+      },
+      async _approveTaskWithVariables(status) {
+        console.log(status);
+        if (status == 1) {
+          const params = {
+            id: this.taskId,
+            reason: this.form.reason,
+            variables: { pass: true }
+          };
+          try {
+            this.isLoading = true;
+            const data = await approveTaskWithVariables(params);
+            if (data.data.code != '-1') {
+              this.$emit('handleAudit', {
+                status,
+                title: '通过'
+              });
+            }
+            this.isLoading = false;
+          } catch (error) {
+            this.isLoading = false;
+          }
+        }
+      }
+    }
+  };
+</script>

+ 125 - 0
src/views/bpm/handleTask/components/onboardingHandling/submit.vue

@@ -0,0 +1,125 @@
+<template>
+  <el-col :span="16" :offset="6">
+    <el-form label-width="100px" ref="formRef" :model="form">
+      <el-form-item
+        label="审批建议"
+        style="margin-bottom: 20px"
+        :rules="{
+          required: true,
+          message: '请选择',
+          trigger: 'change'
+        }"
+      >
+        <el-input
+          type="textarea"
+          v-model="form.reason"
+          placeholder="请输入审批建议"
+        />
+      </el-form-item>
+    </el-form>
+
+    <div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
+      <el-button
+        icon="el-icon-edit-outline"
+        type="success"
+        size="mini"
+        :loading="isLoading"
+        v-click-once
+        @click="handleAudit(1)"
+        >通过
+      </el-button>
+
+      <el-button
+        icon="el-icon-circle-close"
+        type="danger"
+        size="mini"
+        v-click-once
+        :loading="isLoading"
+        @click="outApproveNotPass(0)"
+        >驳回
+      </el-button>
+    </div>
+  </el-col>
+</template>
+<script>
+  import { approveTaskWithVariables, outApproveNotPass } from '@/api/bpm/task';
+  export default {
+    data() {
+      return {
+        form: {
+          reason: '同意'
+        },
+        isLoading: false
+      };
+    },
+    props: {
+      businessId: {
+        default: ''
+      },
+      taskId: {
+        default: ''
+      },
+      id: {
+        default: ''
+      },
+
+      taskDefinitionKey: {
+        default: ''
+      }
+    },
+    methods: {
+      async outApproveNotPass(status) {
+        const params = {
+          id: this.taskId,
+          reason: this.form.reason,
+          outInId: this.businessId
+        };
+        try {
+          this.isLoading = true;
+          const data = await outApproveNotPass(params);
+          if (data.data.code != '-1') {
+            this.$emit('handleAudit', {
+              status,
+              title: '驳回'
+            });
+          }
+          this.isLoading = false;
+        } catch (error) {
+          this.isLoading = false;
+        }
+      },
+
+      handleAudit(status) {
+        if (!this.form.reason && status == 1) {
+          this.$message.warning(`请填写审批意见!`);
+          return;
+        }
+
+        this._approveTaskWithVariables(status);
+      },
+      async _approveTaskWithVariables(status) {
+        console.log(status);
+        if (status == 1) {
+          const params = {
+            id: this.taskId,
+            reason: this.form.reason,
+            variables: { pass: true }
+          };
+          try {
+            this.isLoading = true;
+            const data = await approveTaskWithVariables(params);
+            if (data.data.code != '-1') {
+              this.$emit('handleAudit', {
+                status,
+                title: '通过'
+              });
+            }
+            this.isLoading = false;
+          } catch (error) {
+            this.isLoading = false;
+          }
+        }
+      }
+    }
+  };
+</script>

+ 269 - 0
src/views/bpm/handleTask/components/regularizationHandling/submit.vue

@@ -0,0 +1,269 @@
+<template>
+  <el-col :span="16" :offset="6">
+    <el-form label-width="100px" ref="formRef" :model="form">
+      <!-- <el-form-item
+        label="技术员"
+        prop="technicianId"
+        style="margin-bottom: 20px"
+        :rules="{
+          required: true,
+          message: '请选择',
+          trigger: 'change'
+        }"
+        v-if="taskDefinitionKey == 'productionSupervisorApprove1'"
+      >
+        <el-select
+          v-model="form.technicianId"
+          clearable
+          style="width: 100%"
+          :filterable="true"
+        >
+          <el-option
+            v-for="item in userOptions"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item> -->
+      <el-form-item
+        label="审批建议"
+
+        style="margin-bottom: 20px"
+        :rules="{
+          required: true,
+          message: '请选择',
+          trigger: 'change'
+        }"
+      >
+        <el-input
+          type="textarea"
+          v-model="form.reason"
+          placeholder="请输入审批建议"
+        />
+      </el-form-item>
+    </el-form>
+    <div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
+      <el-button
+        icon="el-icon-edit-outline"
+        type="success"
+        size="mini"
+        v-click-once
+        @click="handleAudit(1)"
+        >通过
+      </el-button>
+      <!-- <el-button
+        icon="el-icon-edit-outline"
+        type="success"
+        size="mini"
+        v-if="taskDefinitionKey === 'productionSupervisorApprove1'"
+        @click="head"
+        >指派技术员
+      </el-button> -->
+      <el-button
+        icon="el-icon-circle-close"
+        type="danger"
+        size="mini"
+        v-click-once
+        @click="handleAudit(0)"
+        v-if="
+          !['starter'].includes(
+            taskDefinitionKey
+          )
+        "
+        >驳回
+      </el-button>
+
+      <el-dropdown @command="(command) => handleCommand(command)" style="margin-left: 30px;">
+        <span class="el-dropdown-link">更多<i class="el-icon-arrow-down el-icon--right"></i></span>
+        <el-dropdown-menu slot="dropdown">
+          <el-dropdown-item command="cancel">作废</el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+
+      <!-- <el-button
+        icon="el-icon-circle-close"
+        type="danger"
+        size="mini"
+        @click="handleBackList"
+        >退回
+      </el-button> -->
+      <!-- <el-button
+        icon="el-icon-circle-close"
+        type="danger"
+        size="mini"
+        @click="handleAudit(0)"
+        v-if="taskDefinitionKey != 'productionSupervisorApprove1'"
+        >不通过
+      </el-button>
+      <el-button
+        icon="el-icon-edit-outline"
+        type="primary"
+        size="mini"
+        v-if="taskDefinitionKey != 'productionSupervisorApprove1'"
+        @click="handleUpdateAssignee"
+        >转办
+      </el-button> -->
+    </div>
+    <head-list ref="headRef" @changeParent="changePersonel"></head-list>
+  </el-col>
+</template>
+
+<script>
+  import {
+    updateTech,
+    UpdateInformation,
+    cancel
+  } from '@/api/bpm/components/saleManage/quotation';
+  import {approveTaskWithVariables, rejectTask,cancelTask} from '@/api/bpm/task';
+  import { listAllUserBind } from '@/api/system/organization';
+  import headList from '@/components/headList';
+  // 流程实例的详情页,可用于审批
+  export default {
+    name: '',
+    components: {
+      headList
+    },
+    props: {
+      businessId: {
+        default: ''
+      },
+      taskId: {
+        default: ''
+      },
+      id: {
+        default: ''
+      },
+      taskDefinitionKey: {
+        default: ''
+      }
+    },
+    data() {
+      return {
+        form: {
+          technicianId: '',
+          reason: '同意'
+        },
+        userOptions: []
+      };
+    },
+    created() {
+      this.userOptions = [];
+      listAllUserBind().then((data) => {
+        this.userOptions.push(...data);
+      });
+    },
+    methods: {
+      /** 处理转办审批人 */
+      handleUpdateAssignee() {
+        this.$emit('handleUpdateAssignee');
+      },
+      /** 退回 */
+      handleBackList() {
+        this.$emit('handleBackList');
+      },
+      head() {
+        this.$refs.headRef.open();
+      },
+      changePersonel(data) {
+        this.form.technicianId = data.id;
+        this.handleAudit(1, 'zp');
+      },
+      async handleAudit(status, type) {
+        //生产主管审批选择技术员
+        // if (this.taskDefinitionKey === 'productionSupervisorApprove1') {
+        //   if (!this.form.technicianId && type == 'zp') {
+        //     this.$message.warning(`请选择技术人员!`);
+        //     return;
+        //   }
+        // }
+        //技术员修改
+        // if (this.taskDefinitionKey === 'technicianApprove' && status === 1) {
+        //   let data = await this.getTableValue();
+        //   if (!arr) {
+        //     return;
+        //   }
+        //   let arr = data.map((item) => {
+        //     return {
+        //       id: item.id,
+        //       technicalAnswerId: item.technicalAnswerId,
+        //       technicalAnswerName: item.technicalAnswerName,
+        //       technicalDrawings: item.technicalDrawings,
+        //       technicalParams: item.technicalParams
+        //     };
+        //   });
+        //   try {
+        //     await updateTech(arr);
+        //   } catch (error) {}
+        // }
+        //销售员补充
+        if ((this.taskDefinitionKey === 'salesmanApprove'||this.taskDefinitionKey === 'starter')&&status === 1) {
+          let arr = await this.getTableValue();
+          if (!arr) {
+            return;
+          }
+          let data = await UpdateInformation(arr);
+          if (data.code != '0') {
+            return;
+          }
+        }
+        this._approveTaskWithVariables(status, this.form.technicianId, type);
+      },
+      async _approveTaskWithVariables(status, technicianId, type) {
+        let variables = {
+          pass: !!status
+        };
+        if (technicianId && type == 'zp') {
+          variables['technicianId'] = technicianId;
+        }
+
+        let API = !!status ? approveTaskWithVariables : rejectTask;
+        API({
+          id: this.taskId,
+          reason: this.form.reason,
+          variables
+        }).then((res) => {
+          if (res.data.code != '-1') {
+            this.$emit('handleAudit', {
+              status,
+              title: status === 0 ? '驳回' : ''
+            });
+          }
+        });
+      },
+
+      getTableValue() {
+        return new Promise((resolve, reject) => {
+          this.$emit('getTableValue', async (data) => {
+            resolve(await data);
+          });
+        });
+      },
+
+      //更多
+      handleCommand(command) {
+        if (command === 'cancel') {
+          this.$confirm("是否确认作废?", {
+            type: 'warning',
+            cancelButtonText: '取消',
+            confirmButtonText: '确定'
+          }).then(() => {
+            cancelTask({
+              id: this.id,
+              taskId: this.taskId,
+              reason: this.form.reason,
+              businessId: this.businessId,
+            }).then(() => {
+              this.$emit('handleClose');
+            }).catch(() => {
+              this.$message.error("流程作废失败");
+            });
+          }).catch(() => {});
+        }
+      },
+
+    }
+  };
+</script>
+
+<style lang="scss"></style>

+ 221 - 0
src/views/bpm/handleTask/components/resignationApplication/submit.vue

@@ -0,0 +1,221 @@
+<template>
+  <el-col :span="16" :offset="6">
+    <el-form label-width="100px" ref="formRef" :model="form">
+      <!-- <el-form-item
+        label="采购员"
+        prop="technicianId"
+        style="margin-bottom: 20px"
+        :rules="{
+          required: true,
+          message: '请选择',
+          trigger: 'change'
+        }"
+      >
+        <el-select
+          v-model="form.technicianId"
+          clearable
+          style="width: 100%"
+          :filterable="true"
+        >
+          <el-option
+            v-for="item in userOptions"
+            @click.native="form.userName = item.name"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item> -->
+      <el-form-item
+        label="审批建议"
+        style="margin-bottom: 20px"
+        :rules="{
+          required: true,
+          message: '请选择',
+          trigger: 'change'
+        }"
+      >
+        <el-input
+          type="textarea"
+          v-model="form.reason"
+          placeholder="请输入审批建议"
+        />
+      </el-form-item>
+    </el-form>
+    <div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
+      <el-button
+        icon="el-icon-edit-outline"
+        type="success"
+        size="mini"
+        @click="handleAudit(1)"
+        v-click-once
+        >通过
+      </el-button>
+
+      <el-button
+        icon="el-icon-circle-close"
+        type="danger"
+        size="mini"
+        v-click-once
+        @click="rejectTask(0)"
+        >驳回
+      </el-button>
+
+      <el-dropdown
+        @command="(command) => handleCommand(command)"
+        style="margin-left: 30px"
+      >
+        <span class="el-dropdown-link"
+          >更多<i class="el-icon-arrow-down el-icon--right"></i
+        ></span>
+        <el-dropdown-menu slot="dropdown">
+          <el-dropdown-item command="cancel">作废</el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+  </el-col>
+</template>
+
+<script>
+  import {
+    apspurchaseplan,
+    cancel
+  } from '@/api/bpm/components/apsMeterialPlan';
+  import {
+    approveTaskWithVariables,
+    rejectTask,
+    cancelTask
+  } from '@/api/bpm/task';
+  import { listAllUserBind } from '@/api/system/organization';
+
+  // 流程实例的详情页,可用于审批
+  export default {
+    name: '',
+    components: {},
+    props: {
+      businessId: {
+        default: ''
+      },
+      taskId: {
+        default: ''
+      },
+      id: {
+        default: ''
+      },
+
+      taskDefinitionKey: {
+        default: ''
+      }
+    },
+    data() {
+      return {
+        form: {
+          // technicianId: '',
+          reason: '同意'
+        },
+        userOptions: []
+      };
+    },
+    created() {
+      this.userOptions = [];
+      listAllUserBind().then((data) => {
+        this.userOptions.push(...data);
+      });
+    },
+    methods: {
+      async handleAudit(status, type) {
+        //生产主管审批选择技术员
+
+        // if (!this.form.technicianId && status == 1) {
+        //   this.$message.warning(`请选择采购员!`);
+        //   return;
+        // }
+
+        this._approveTaskWithVariables(status);
+      },
+      rejectTask(status) {
+        rejectTask({
+          id: this.taskId,
+          reason: this.form.reason,
+          pass: false
+        }).then((res) => {
+          if (res.data.code != '-1') {
+            this.$emit('handleAudit', {
+              status,
+              title: '驳回'
+            });
+          }
+        });
+      },
+      async _approveTaskWithVariables(status) {
+        if (status == 1) {
+          apspurchaseplan({
+            businessId: this.businessId,
+            id: this.taskId,
+            userId: this.form.technicianId,
+            userName: this.form.userName,
+            reason: this.form.reason,
+            pass: true
+          }).then((res) => {
+            if (res.data.code != '-1') {
+              this.$emit('handleAudit', {
+                status,
+                title: ''
+              });
+            }
+          });
+        } else if (status == 0) {
+          let API = rejectTask;
+          API({
+            id: this.taskId,
+            reason: this.form.reason,
+            pass: false
+          }).then((res) => {
+            if (res.data.code != '-1') {
+              this.$emit('handleAudit', {
+                status,
+                title: '驳回'
+              });
+            }
+          });
+        }
+      },
+
+      getTableValue() {
+        return new Promise((resolve, reject) => {
+          this.$emit('getTableValue', async (data) => {
+            resolve(await data);
+          });
+        });
+      },
+
+      //更多
+      handleCommand(command) {
+        if (command === 'cancel') {
+          this.$confirm('是否确认作废?', {
+            type: 'warning',
+            cancelButtonText: '取消',
+            confirmButtonText: '确定'
+          })
+            .then(() => {
+              cancelTask({
+                taskId: this.taskId,
+                id: this.id,
+                reason: this.form.reason,
+                businessId: this.businessId
+              })
+                .then(() => {
+                  this.$emit('handleClose');
+                })
+                .catch(() => {
+                  this.$message.error('流程作废失败');
+                });
+            })
+            .catch(() => {});
+        }
+      }
+    }
+  };
+</script>
+
+<style lang="scss"></style>

+ 104 - 13
src/views/bpm/handleTask/index.vue

@@ -447,11 +447,78 @@
           )
         );
       },
+      /** 解析流程模型里配置的表单路由 JSON:{ pcView, pcHandle, ... } */
+      parseFormRouteConfig(raw) {
+        if (!raw) return null;
+        if (typeof raw === 'object') return raw;
+        const text = String(raw).trim();
+        if (!text || text[0] !== '{') return null;
+        try {
+          return JSON.parse(text);
+        } catch (e) {
+          return null;
+        }
+      },
+      /** 规范化为 /bpm/.../*.vue,便于 require(`@/views${path}`) */
+      normalizeViewPath(path) {
+        let text = String(path || '').trim();
+        if (!text) return '';
+        text = text.replace(/\\/g, '/');
+        text = text.replace(/^@\/views/, '');
+        text = text.replace(/^src\/views/, '');
+        if (!text.startsWith('/')) text = `/${text}`;
+        if (text === '/') return '';
+        return text;
+      },
+      /**
+       * 解析 PC 详情/处理组件路径
+       * 优先待办带回的 pcViewRouter/pcHandleRouter,
+       * 其次流程定义 formCustomCreatePath / formKey(pcView、pcHandle)
+       */
+      resolveFormRouters() {
+        const fromTodo = {
+          view: this.normalizeViewPath(this.listData.pcViewRouter),
+          handle: this.normalizeViewPath(this.listData.pcHandleRouter)
+        };
+        const def = this.processInstance?.processDefinition || {};
+        const fromDef =
+          this.parseFormRouteConfig(def.formCustomCreatePath) ||
+          this.parseFormRouteConfig(def.formKey) ||
+          this.parseFormRouteConfig(this.listData.formKey) ||
+          this.parseFormRouteConfig(this.listData.formCustomCreatePath) ||
+          {};
+        // 录用审批:流程未配路由时使用本项目约定路径
+        const businessKey = String(
+          def.key ||
+            this.listData.businessKey ||
+            this.listData.processDefinitionKey ||
+            ''
+        );
+        const hiringFallback =
+          businessKey === 'hr_recruit_hiring_approval'
+            ? {
+                pcView:
+                  '/bpm/handleTask/components/hiringApproval/hiringApprovalDetail.vue',
+                pcHandle:
+                  '/bpm/handleTask/components/hiringApproval/submit.vue'
+              }
+            : {};
+        return {
+          view:
+            fromTodo.view ||
+            this.normalizeViewPath(fromDef.pcView || fromDef.pcViewRouter) ||
+            this.normalizeViewPath(hiringFallback.pcView),
+          handle:
+            fromTodo.handle ||
+            this.normalizeViewPath(
+              fromDef.pcHandle || fromDef.pcHandleRouter
+            ) ||
+            this.normalizeViewPath(hiringFallback.pcHandle)
+        };
+      },
       getBusinessComponentRouter(type) {
-        const router =
-          type === 'view'
-            ? this.listData.pcViewRouter
-            : this.listData.pcHandleRouter;
+        const routers = this.resolveFormRouters();
+        const router = type === 'view' ? routers.view : routers.handle;
         if (this.isStockBatchChange()) {
           const invalidViewRouter =
             type === 'view' &&
@@ -462,7 +529,38 @@
             }.vue`;
           }
         }
-        return router;
+        return router || '';
+      },
+      registerAsyncBizComponents() {
+        const viewRouter = this.getBusinessComponentRouter('view');
+        const handleRouter = this.getBusinessComponentRouter('handle');
+        if (!viewRouter || !handleRouter) {
+          console.error('流程表单路由未配置', {
+            pcViewRouter: this.listData.pcViewRouter,
+            pcHandleRouter: this.listData.pcHandleRouter,
+            formCustomCreatePath:
+              this.processInstance?.processDefinition?.formCustomCreatePath,
+            formKey: this.processInstance?.processDefinition?.formKey,
+            resolved: { viewRouter, handleRouter },
+            businessCode: this.listData.businessCode,
+            businessType: this.listData.businessType
+          });
+          this.$message.error(
+            '流程未配置 PC 表单路由。请在流程模型「业务表单」或节点 formKey 中配置:{"pcView":"/bpm/handleTask/components/.../detail.vue","pcHandle":"/bpm/handleTask/components/.../submit.vue"}'
+          );
+          return false;
+        }
+        // 回填到 listData,便于后续使用
+        this.listData.pcViewRouter = viewRouter;
+        this.listData.pcHandleRouter = handleRouter;
+        console.log('加载业务表单组件', { viewRouter, handleRouter });
+        Vue.component('async-biz-form-component', (resolve) => {
+          require([`@/views${viewRouter}`], resolve);
+        });
+        Vue.component('async-sub-form-component', (resolve) => {
+          require([`@/views${handleRouter}`], resolve);
+        });
+        return true;
       },
       submit(data) {
         this.$refs.bziRef.save(data);
@@ -497,14 +595,7 @@
           // const bizpath = formCustomCreatePath.pcView;
           // const subpath = formCustomCreatePath.pcHandle;
 
-          Vue.component('async-biz-form-component', (resolve) => {
-            const router = this.getBusinessComponentRouter('view');
-            require([`@/views${router}`], resolve);
-          });
-          Vue.component('async-sub-form-component', (resolve) => {
-            const router = this.getBusinessComponentRouter('handle');
-            require([`@/views${router}`], resolve);
-          });
+          this.registerAsyncBizComponents();
           //   // 设置表单信息
           //   if (this.processInstance.processDefinition.formType === 10) {
           //     this.detailForm = {

+ 10 - 1
src/views/bpm/todo/index.vue

@@ -326,7 +326,16 @@
               taskId: row.id,
               taskDefinitionKey: row.taskDefinitionKey,
               pcHandleRouter: row.pcHandleRouter,
-              pcViewRouter: row.pcViewRouter
+              pcViewRouter: row.pcViewRouter,
+              formKey: row.formKey,
+              formCustomCreatePath: row.formCustomCreatePath,
+              businessKey:
+                row.businessKey ||
+                row.processInstance?.processDefinitionId ||
+                row.processInstance?.processDefinition?.key,
+              processDefinitionKey:
+                row.processDefinitionKey ||
+                row.processInstance?.processDefinition?.key
             });
           }
         } else {