Просмотр исходного кода

物品领用审批,转正审批,离职申请审批

ZGC4846 6 дней назад
Родитель
Сommit
e90cc973ed

+ 13 - 3
src/api/bpm/components/positionApplication/index.js

@@ -12,7 +12,9 @@ function splitText(value) {
 export function adaptPositionApplication(raw = {}) {
   return {
     id: raw.id,
-    code: raw.positionCode || '',
+    code: raw.documentCode || raw.positionCode || '',
+    positionCode: raw.positionCode || '',
+    documentCode: raw.documentCode || '',
     positionName: raw.positionName || '',
     organizationId: raw.organizationId,
     organizationName: raw.organizationName || '',
@@ -20,17 +22,22 @@ export function adaptPositionApplication(raw = {}) {
     deptName: raw.deptName || '',
     positionLevel: raw.positionLevel || '',
     positionSequence: raw.positionSequence || '',
-    type: raw.positionType || '',
+    // 岗位类型:后端已返回中文 positionTypeName 优先,其次原始值 positionType
+    type: raw.positionTypeName || raw.positionType || '',
+    rawType: raw.positionType || '',
     contractTemplateId: raw.contractTemplateId,
     contractTemplateName: raw.contractTemplateName || '',
     headcount: Number(raw.plannedRecruitmentCount ?? 0),
     applicant: raw.applicantName || '',
+    applicantUserId: raw.applicantUserId,
     status: raw.status || 'DRAFT',
+    processInstanceId: raw.processInstanceId || '',
+    effectivePositionId: raw.effectivePositionId,
     updatedAt: raw.updateTime || raw.createTime || '',
     reviewer: raw.reviewerName || '',
+    reviewerUserId: raw.reviewerUserId,
     reviewTime: raw.reviewTime || '',
     reviewOpinion: raw.reviewOpinion || '',
-    effectivePositionId: raw.effectivePositionId,
     operationLogs: raw.operationLogs || []
   };
 }
@@ -47,9 +54,12 @@ export function adaptPositionApplicationDetail(raw = {}) {
     salaryMax: Number(salary.salaryMax ?? 0),
     expYears: Number(salary.experienceYears ?? 0),
     education: salary.educationRequirement || '',
+    genderRequirement: salary.genderRequirement || '',
+    ageRange: [salary.ageMin, salary.ageMax].filter(Boolean).join(' - '),
     industryExp: salary.industryExperience || '',
     professionalAbility: salary.professionalAbility || '',
     otherRequirement: salary.otherRequirement || '',
+    workShift: salary.workShift || '',
     skills: (raw.skills || []).map((item) => ({
       ...item,
       name: item.skillName || '',

+ 18 - 0
src/api/main/index.js

@@ -40,4 +40,22 @@ export async function getOnboardingItemReceiptsById(id) {
     return res.data.data;
   }
   return Promise.reject(new Error(res.data.message));
+}
+
+/** 转正单详情查询 GET /main/regularizations/{id} */
+export async function getRegularizationById(id) {
+  const res = await request.get(`/main/regularizations/${id}`);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+/** 离职单详情查询 GET /main/resignations/{id} */
+export async function getResignationById(id) {
+  const res = await request.get(`/main/resignations/${id}`);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
 }

+ 11 - 7
src/api/system/file/index.js

@@ -46,19 +46,23 @@ export async function getPathAddress () {
 }
 
 /**
- * 获取文件
+ * 获取文件(通用:支持 objectName 路径 或 fileId 主键)
  */
 export async function getFile (params, fileName) {
   const res = await request.get('/main/file/getFile', {
     params,
     responseType: 'blob'
   });
-  const arr = params.objectName.split('/');
-  download(res.data, fileName || arr[arr.length - 1]);
-  // if (res.data.code === '0') {
-  //   return res.data.data;
-  // }
-  // return Promise.reject();
+  let defaultName = fileName;
+  if (!defaultName) {
+    if (params.objectName) {
+      const arr = params.objectName.split('/');
+      defaultName = arr[arr.length - 1];
+    } else if (params.fileId) {
+      defaultName = `file_${params.fileId}`;
+    }
+  }
+  download(res.data, defaultName);
 }
 /**
  * 删除文件

+ 23 - 1
src/components/addDoc/index.vue

@@ -27,6 +27,8 @@
 
 <script>
   import fileUpload from './main.vue';
+  import { queryIds } from './api';
+  import { setFileUrl } from './util.js';
 
   export default {
     name: 'index',
@@ -70,7 +72,27 @@
     },
     created() {},
     methods: {
-      handleUpload() {
+      /**
+       * 点击查看/上传按钮
+       * - 查看模式且只有一个文件时,跳过列表弹窗直接预览
+       * - 其他情况打开列表弹窗
+       */
+      async handleUpload() {
+        // 查看模式且只有一个文件 → 直接预览
+        if (this.type === 'view' && this.selectVal.length === 1) {
+          try {
+            const list = await queryIds({
+              ids: "'" + this.selectVal + "'"
+            });
+            if (list && list.length) {
+              window.open(setFileUrl(list[0]));
+              return;
+            }
+          } catch (e) {
+            // 预览失败则降级为打开列表弹窗
+          }
+        }
+        // 多个文件或非查看模式 → 打开列表弹窗
         this.$refs.fileUploadRef.open(this.selectVal, this.type);
       },
       getFiles(val = []) {

+ 42 - 30
src/styles/views/positionApplication/index.scss

@@ -3,15 +3,15 @@
   --ink: #203047;
   --muted: #7c899c;
   min-height: 100%;
-  padding: 22px;
+  padding: 12px;
   color: var(--ink);
   background: radial-gradient(circle at 7% 0, rgba(32, 183, 216, 0.08), transparent 24%),
     radial-gradient(circle at 94% 3%, rgba(23, 104, 229, 0.08), transparent 22%), #f3f6fb;
 }
 
 .detail-page-header {
-  margin-bottom: 16px;
-  padding: 16px 18px;
+  margin-bottom: 8px;
+  padding: 10px 14px;
   border: 1px solid #e0e7f1;
   border-radius: 9px;
   background: #fff;
@@ -46,7 +46,7 @@
 .drawer-title p {
   margin: 4px 0 0;
   color: #8b97a7;
-  font-size: 9px;
+  font-size: 14px;
 }
 .drawer-title > em {
   margin-left: auto;
@@ -54,7 +54,7 @@
   align-items: center;
   gap: 5px;
   color: #168d69;
-  font-size: 10px;
+  font-size: 14px;
   font-style: normal;
   white-space: nowrap;
 }
@@ -84,8 +84,8 @@
 }
 
 .detail-overview {
-  margin-bottom: 15px;
-  padding: 16px;
+  margin-bottom: 8px;
+  padding: 10px 14px;
   display: flex;
   align-items: center;
   gap: 12px;
@@ -102,7 +102,7 @@
   border-radius: 9px;
   color: #1768e5;
   background: #eaf2ff;
-  font-size: 12px;
+  font-size: 14px;
   font-weight: 700;
 }
 .type-管理岗 {
@@ -125,20 +125,20 @@
 .detail-overview p {
   margin: 5px 0 0;
   color: #8290a3;
-  font-size: 10px;
+  font-size: 14px;
 }
 
 .detail-section {
-  padding: 18px;
+  padding: 10px 14px;
   border: 1px solid #e1e7ef;
   border-radius: 8px;
   background: #fff;
 }
 .detail-section + .detail-section {
-  margin-top: 14px;
+  margin-top: 8px;
 }
 .detail-heading {
-  margin-bottom: 17px;
+  margin-bottom: 10px;
   display: flex;
   align-items: center;
   gap: 10px;
@@ -160,25 +160,37 @@
 .detail-heading p {
   margin: 4px 0 0;
   color: #8a96a7;
-  font-size: 9px;
+  font-size: 14px;
 }
 .detail-grid {
   display: grid;
-  grid-template-columns: repeat(2, minmax(0, 1fr));
-  gap: 16px 24px;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 8px 24px;
+}
+.detail-grid > div {
+  display: flex;
+  align-items: flex-start;
+  gap: 6px;
+  line-height: 22px;
 }
 .detail-grid span,
 .detail-block > span {
-  display: block;
+  display: inline;
+  color: #8a96a7;
+  font-size: 14px;
+  white-space: nowrap;
+}
+.detail-grid span::after {
+  content: ':';
   color: #8a96a7;
-  font-size: 10px;
 }
 .detail-grid strong {
-  margin-top: 5px;
-  display: block;
+  margin-top: 0;
+  display: inline;
   color: #3d4d64;
-  font-size: 12px;
-  font-weight: 600;
+  font-size: 14px;
+  font-weight: 500;
+  word-break: break-all;
 }
 .detail-block {
   margin-top: 14px;
@@ -187,13 +199,13 @@
   margin: 8px 0 0;
   padding-left: 19px;
   color: #59677b;
-  font-size: 11px;
+  font-size: 14px;
   line-height: 1.8;
 }
 .review-opinion {
   margin: 7px 0 0;
   color: #59677b;
-  font-size: 11px;
+  font-size: 14px;
   line-height: 1.65;
 }
 .log-list {
@@ -217,17 +229,17 @@
   border-radius: 5px;
   color: #58739b;
   background: #f0f5fc;
-  font-size: 10px;
+  font-size: 14px;
   text-align: center;
 }
 .log-list li p {
   margin: 0;
   color: #59677b;
-  font-size: 11px;
+  font-size: 14px;
 }
 .log-list li em {
   color: #9aa5b4;
-  font-size: 10px;
+  font-size: 14px;
   font-style: normal;
   white-space: nowrap;
 }
@@ -243,13 +255,13 @@
   border-radius: 5px;
   color: #58739b;
   background: #f7faff;
-  font-size: 10px;
+  font-size: 14px;
   font-style: normal;
 }
 .detail-muted {
   margin: 8px 0 0;
   color: #9aa5b4;
-  font-size: 10px;
+  font-size: 14px;
 }
 .detail-audit {
   margin-top: 14px;
@@ -260,7 +272,7 @@
   gap: 12px;
   color: #8793a3;
   background: #f8fafd;
-  font-size: 9px;
+  font-size: 14px;
   border-radius: 8px;
 }
 .detail-audit i {
@@ -270,7 +282,7 @@
 
 @media (max-width: 760px) {
   .application-page {
-    padding: 14px;
+    padding: 8px;
   }
   .detail-grid {
     grid-template-columns: 1fr;

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

@@ -1,3 +1,4 @@
+<!-- 录用审批详情 -->
 <template>
   <main class="hiring-approval-page" v-loading="loading">
     <header class="detail-page-header">
@@ -148,7 +149,6 @@
                 </el-form-item>
               </el-col>
             </el-row>
-            <p class="field-hint">先选所属机构,再选入职部门,再按部门加载应聘岗位。</p>
           </section>
 
           <section class="info-card">

+ 13 - 74
src/views/bpm/handleTask/components/positionApplication/positionApplicationTable.vue

@@ -1,3 +1,4 @@
+<!-- 岗位申请详情 -->
 <template>
   <main class="application-page" v-loading="loading">
     <header class="detail-page-header">
@@ -30,7 +31,6 @@
         <span><i class="el-icon-document"></i></span>
         <div>
           <h3>申请信息</h3>
-          <p>岗位申请单据的关键信息</p>
         </div>
       </div>
       <div class="detail-grid">
@@ -74,51 +74,14 @@
       </div>
     </section>
 
-    <section
-      class="detail-section"
-      v-if="form.reviewer || form.reviewTime || (form.operationLogs && form.operationLogs.length)"
-    >
-      <div class="detail-heading">
-        <span><i class="el-icon-s-check"></i></span>
-        <div>
-          <h3>审批记录</h3>
-          <p>最终审批结果与意见</p>
-        </div>
-      </div>
-      <div class="detail-grid">
-        <div>
-          <span>审批人</span><strong>{{ form.reviewer || '-' }}</strong>
-        </div>
-        <div>
-          <span>审批时间</span><strong>{{ form.reviewTime || '-' }}</strong>
-        </div>
-      </div>
-      <div class="detail-block">
-        <span>审批意见</span>
-        <p class="review-opinion">{{ form.reviewOpinion || '暂无审批意见' }}</p>
-      </div>
-      <ol
-        class="log-list"
-        v-if="form.operationLogs && form.operationLogs.length"
-      >
-        <li v-for="log in form.operationLogs" :key="log.id || log.createTime">
-          <span>{{ operationName(log.operationType) }}</span>
-          <p>{{ log.operationRemark || '-' }}</p>
-          <em>{{ log.createTime }}</em>
-        </li>
-      </ol>
-    </section>
-
     <section class="detail-section">
       <div class="detail-heading">
         <span><i class="el-icon-s-cooperation"></i></span>
         <div>
           <h3>岗位职责</h3>
-          <p>该岗位的核心工作职责</p>
         </div>
       </div>
       <div class="detail-block">
-        <span>岗位职责</span>
         <ol class="duty-list" v-if="form.duties && form.duties.length">
           <li v-for="(item, index) in form.duties" :key="index">{{ item }}</li>
         </ol>
@@ -131,7 +94,6 @@
         <span><i class="el-icon-user"></i></span>
         <div>
           <h3>技能要求</h3>
-          <p>岗位任职能力、学历经验与技能证书要求</p>
         </div>
       </div>
       <div class="detail-grid">
@@ -195,7 +157,6 @@ import {
   getContractTemplatePage
 } from '@/api/bpm/components/positionApplication';
 import { listOrganizations } from '@/api/system/organization';
-import { getDictByCode } from '@/api/sys';
 
 const STATUS_LABELS = {
   DRAFT: '未提交',
@@ -211,12 +172,15 @@ const OPERATION_LABELS = {
   SUBMIT: '提交审批',
   APPROVE: '审批通过',
   REJECT: '驳回',
-  EFFECTIVE: '生效'
+  EFFECTIVE: '生效',
+  CANCEL: '取消'
 };
 
 const emptyForm = () => ({
   id: null,
   code: '',
+  positionCode: '',
+  documentCode: '',
   positionName: '',
   organizationId: null,
   organizationName: '',
@@ -225,6 +189,7 @@ const emptyForm = () => ({
   positionLevel: '',
   positionSequence: '',
   type: '',
+  rawType: '',
   contractTemplateId: null,
   contractTemplateName: '',
   headcount: 0,
@@ -233,14 +198,18 @@ const emptyForm = () => ({
   salaryMax: 0,
   expYears: 0,
   education: '',
+  genderRequirement: '',
+  ageRange: '',
   industryExp: '',
   professionalAbility: '',
   otherRequirement: '',
+  workShift: '',
   skills: [],
   certificates: [],
   applicant: '',
   duties: [],
   status: '',
+  processInstanceId: '',
   reviewer: '',
   reviewTime: '',
   reviewOpinion: '',
@@ -260,7 +229,6 @@ export default {
       loading: false,
       form: emptyForm(),
       orgMap: {},
-      typeOptions: [],
       contractTemplates: []
     };
   },
@@ -289,7 +257,6 @@ export default {
   created() {
     this.loadOrganizations();
     this.loadContractTemplates();
-    this.loadTypeOptions();
   },
   methods: {
     async getInfo(id) {
@@ -299,8 +266,7 @@ export default {
         const detail = await getPositionApplicationById(id);
         this.form = {
           ...emptyForm(),
-          ...detail,
-          type: this.resolveTypeValue(detail.type)
+          ...detail
         };
       } catch (error) {
         this.form = emptyForm();
@@ -334,34 +300,6 @@ export default {
         this.contractTemplates = [];
       }
     },
-    async loadTypeOptions() {
-      try {
-        const items = await getDictByCode('position_nature');
-        this.typeOptions = (items || [])
-          .map((item) => {
-            const text = String(item.value ?? item.label ?? '');
-            return {
-              label: text,
-              value: text,
-              key: item.key != null ? String(item.key) : text
-            };
-          })
-          .filter((item) => item.value);
-      } catch (error) {
-        this.typeOptions = [];
-      }
-    },
-    resolveTypeValue(raw) {
-      if (raw === null || raw === undefined || raw === '') return '';
-      const text = String(raw);
-      const hit = this.typeOptions.find(
-        (item) =>
-          String(item.value) === text ||
-          String(item.label) === text ||
-          String(item.key) === text
-      );
-      return hit ? hit.value : text;
-    },
     orgName(row = {}) {
       const id = row.organizationId;
       if (id === null || id === undefined || id === '') {
@@ -382,8 +320,9 @@ export default {
       );
     },
     typeName(value) {
+      // 后端已返回中文 positionTypeName,API adapt 时已优先用中文
       if (value === null || value === undefined || value === '') return '-';
-      return this.resolveTypeValue(value) || '-';
+      return String(value);
     },
     levelText(value) {
       if (value === null || value === undefined || value === '') return '';

+ 521 - 0
src/views/bpm/handleTask/components/regularizationHandling/regularizationHandlingDetail.vue

@@ -0,0 +1,521 @@
+<!-- 转正申请详情 -->
+<template>
+  <main class="regularization-detail" v-loading="loading">
+    <!-- 员工信息 -->
+    <section class="detail-section">
+      <div class="info-card">
+        <div class="info-card__head">
+          <i class="el-icon-user-solid"></i>
+          <span>员工信息</span>
+          <span v-if="approvalStatusText" class="info-card__code">
+            <el-tag :type="approvalStatusTag" size="mini">{{ approvalStatusText }}</el-tag>
+          </span>
+        </div>
+        <div class="info-card__body">
+          <div class="info-item">
+            <span class="info-item__label">申请员工</span>
+            <span class="info-item__value">{{ employeeInfo.userName || '-' }}
+              <span v-if="employeeInfo.jobNumber" class="info-item__sub">({{ employeeInfo.jobNumber }})</span>
+            </span>
+          </div>
+          <div class="info-item">
+            <span class="info-item__label">手机号</span>
+            <span class="info-item__value">{{ employeeInfo.mobile || '-' }}</span>
+          </div>
+          <div class="info-item">
+            <span class="info-item__label">所属机构</span>
+            <span class="info-item__value">{{ employeeInfo.groupName || '-' }}</span>
+          </div>
+          <div class="info-item">
+            <span class="info-item__label">所属部门</span>
+            <span class="info-item__value">{{ employeeInfo.deptName || '-' }}</span>
+          </div>
+          <div class="info-item">
+            <span class="info-item__label">岗位</span>
+            <span class="info-item__value">{{ employeeInfo.postName || employeeInfo.positionName || '-' }}</span>
+          </div>
+          <div class="info-item">
+            <span class="info-item__label">直属上级</span>
+            <span class="info-item__value">{{ employeeInfo.managerName || '-' }}</span>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <!-- 转正信息(表单可操作) -->
+    <section class="detail-section">
+      <div class="info-card">
+        <div class="info-card__head">
+          <i class="el-icon-date"></i>
+          <span>转正信息</span>
+        </div>
+        <el-form
+          ref="formRef"
+          class="regular-form"
+          :model="form"
+          :rules="formRules"
+          label-width="110px"
+          label-position="right"
+        >
+          <div class="form-row">
+            <el-form-item label="入职日期" prop="joinDate">
+              <el-date-picker
+                v-model="form.joinDate"
+                type="date"
+                value-format="yyyy-MM-dd"
+                placeholder="请选择入职日期"
+                style="width: 100%"
+                :disabled="isReadonly"
+              />
+            </el-form-item>
+            <el-form-item label="转正类型" prop="regularizationType">
+              <el-select
+                v-model="form.regularizationType"
+                placeholder="请选择转正类型"
+                style="width: 100%"
+                :disabled="isReadonly"
+                @change="onRegularizationTypeChange"
+              >
+                <el-option
+                  v-for="opt in regularizationTypeOptions"
+                  :key="opt.value"
+                  :label="opt.label"
+                  :value="opt.value"
+                />
+              </el-select>
+            </el-form-item>
+          </div>
+
+          <div class="form-row">
+            <el-form-item label="计划转正日期" prop="plannedRegularDate">
+              <el-date-picker
+                v-model="form.plannedRegularDate"
+                type="date"
+                value-format="yyyy-MM-dd"
+                placeholder="请选择计划转正日期"
+                style="width: 100%"
+                :disabled="isReadonly"
+              />
+            </el-form-item>
+            <el-form-item label="实际生效日期" prop="effectiveDate">
+              <el-date-picker
+                v-model="form.effectiveDate"
+                type="date"
+                value-format="yyyy-MM-dd"
+                placeholder="留空按计划日期生效"
+                style="width: 100%"
+                :disabled="isReadonly"
+              />
+            </el-form-item>
+          </div>
+
+          <!-- 延期转正专属字段 -->
+          <template v-if="form.regularizationType === 'EXTENDED'">
+            <div class="form-row form-row--extended">
+              <div class="extended-title">
+                <i class="el-icon-warning-outline"></i>
+                <span>延期转正信息</span>
+              </div>
+            </div>
+            <div class="form-row">
+              <el-form-item label="延期天数" prop="extensionDays">
+                <el-input-number
+                  v-model="form.extensionDays"
+                  :min="1"
+                  :max="365"
+                  :step="1"
+                  placeholder="请输入延期天数"
+                  style="width: 100%"
+                  :disabled="isReadonly"
+                />
+              </el-form-item>
+              <el-form-item label="延期后试用期结束" prop="newProbationEndDate">
+                <el-date-picker
+                  v-model="form.newProbationEndDate"
+                  type="date"
+                  value-format="yyyy-MM-dd"
+                  placeholder="选择延期后试用期结束日期"
+                  style="width: 100%"
+                  :disabled="isReadonly"
+                />
+              </el-form-item>
+            </div>
+            <div class="form-row">
+              <el-form-item label="延期原因" prop="extensionReason">
+                <el-input
+                  v-model="form.extensionReason"
+                  type="textarea"
+                  :rows="3"
+                  placeholder="请填写延期转正的原因(必填)"
+                  :disabled="isReadonly"
+                />
+              </el-form-item>
+            </div>
+          </template>
+
+          <div class="form-row">
+            <el-form-item label="签署方式">
+              <span class="plain-text">{{ signWayText }}</span>
+            </el-form-item>
+            <el-form-item label="申请资料">
+              <fileMain
+                v-model="approvalFileIds"
+                type="view"
+                size="small"
+                :view-disabled="!approvalFileIds.length"
+              />
+            </el-form-item>
+          </div>
+        </el-form>
+      </div>
+    </section>
+  </main>
+</template>
+
+<script>
+import { getRegularizationById } from '@/api/main';
+import { getEmployeeById } from '@/api/organization';
+
+const empty = () => ({
+  id: '',
+  userId: null,
+  joinDate: '',
+  plannedRegularDate: '',
+  effectiveDate: '',
+  regularizationType: '',
+  extensionDays: null,
+  extensionReason: '',
+  newProbationEndDate: '',
+  signWay: 'ONLINE',
+  approvalDocumentFileId: null,
+  userSummary: '',
+  managerEvaluation: '',
+  hrComment: '',
+  approvalStatus: '',
+  result: ''
+});
+
+const REGULARIZATION_TYPE_OPTIONS = [
+  { value: 'NORMAL', label: '按期转正' },
+  { value: 'EARLY', label: '提前转正' },
+  { value: 'EXTENDED', label: '延期转正' },
+  { value: 'FAILED', label: '转正不通过' }
+];
+
+const APPROVAL_STATUS_MAP = {
+  DRAFT: { text: '草稿', tag: 'info' },
+  APPROVING: { text: '审批中', tag: 'warning' },
+  APPROVED: { text: '已通过', tag: 'success' },
+  REJECTED: { text: '已驳回', tag: 'danger' },
+  COMPLETED: { text: '已完成', tag: 'success' }
+};
+
+/** 从多种字段名中取值 */
+function pick(obj, keys) {
+  if (obj == null || typeof obj !== 'object') return '';
+  for (let i = 0; i < keys.length; i++) {
+    const v = obj[keys[i]];
+    if (v !== undefined && v !== null && v !== '') return v;
+  }
+  return '';
+}
+
+export default {
+  name: 'RegularizationHandlingDetail',
+  props: {
+    businessId: {
+      default: ''
+    },
+    /** 是否只读(BPM 详情页一般只读) */
+    readonly: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      form: empty(),
+      employeeInfo: {},
+      approvalDocumentFileId: null,
+      approvalFileIds: [],
+      regularizationTypeOptions: REGULARIZATION_TYPE_OPTIONS
+    };
+  },
+  computed: {
+    isReadonly() {
+      return this.readonly;
+    },
+    formRules() {
+      const base = {
+        regularizationType: [
+          { required: true, message: '请选择转正类型', trigger: 'change' }
+        ],
+        plannedRegularDate: [
+          { required: true, message: '请选择计划转正日期', trigger: 'change' }
+        ]
+      };
+      if (this.form.regularizationType === 'EXTENDED') {
+        base.extensionDays = [
+          { required: true, message: '请输入延期天数', trigger: 'blur' },
+          { type: 'number', min: 1, message: '延期天数必须大于 0', trigger: 'blur' }
+        ];
+        base.extensionReason = [
+          { required: true, message: '请填写延期原因', trigger: 'blur' }
+        ];
+      }
+      return base;
+    },
+    approvalStatusText() {
+      const key = String(this.form.approvalStatus || '').toUpperCase();
+      return APPROVAL_STATUS_MAP[key]?.text || '';
+    },
+    approvalStatusTag() {
+      const key = String(this.form.approvalStatus || '').toUpperCase();
+      return APPROVAL_STATUS_MAP[key]?.tag || 'info';
+    },
+    signWayText() {
+      const map = { ONLINE: '线上签署', OFFLINE: '线下签署' };
+      return map[this.form.signWay] || this.form.signWay || '-' ;
+    }
+  },
+  watch: {
+    businessId: {
+      immediate: true,
+      handler(id) {
+        if (id) this.bootstrap(id);
+      }
+    }
+  },
+  methods: {
+    onRegularizationTypeChange(val) {
+      // 切换为非延期转正时,清空延期相关字段
+      if (val !== 'EXTENDED') {
+        this.form.extensionDays = null;
+        this.form.extensionReason = '';
+        this.form.newProbationEndDate = '';
+      }
+      if (this.$refs.formRef) {
+        this.$nextTick(() => this.$refs.formRef.clearValidate());
+      }
+    },
+    /** 将 swagger 字段 → 组件 form */
+    mapRegularization(data) {
+      if (!data || typeof data !== 'object') return empty();
+      return {
+        id: data.id ?? '',
+        userId: pick(data, ['userId']),
+        joinDate: this.toDateStr(pick(data, ['joinDate', 'joinDateTime', 'hireDate'])),
+        plannedRegularDate: this.toDateStr(pick(data, ['plannedRegularDate'])),
+        effectiveDate: this.toDateStr(pick(data, ['effectiveDate'])),
+        regularizationType: pick(data, ['regularizationType']),
+        extensionDays: pick(data, ['extensionDays']) ?? null,
+        extensionReason: pick(data, ['extensionReason']),
+        newProbationEndDate: this.toDateStr(pick(data, ['newProbationEndDate'])),
+        signWay: pick(data, ['signWay', 'signatureWay']) || 'ONLINE',
+        approvalDocumentFileId: pick(data, ['approvalDocumentFileId']),
+        userSummary: pick(data, ['userSummary']),
+        managerEvaluation: pick(data, ['managerEvaluation']),
+        hrComment: pick(data, ['hrComment']),
+        approvalStatus: pick(data, ['approvalStatus']),
+        result: pick(data, ['result'])
+      };
+    },
+    /** 员工信息:转正单接口优先,缺失则从 getEmployeeById 补
+     *  真实原始字段名对照 HR adaptEmployee:
+     *  - 转正单 raw: employeeName / employeeNo / groupName / deptName / positionName / managerUserName
+     *  - 员工档案 raw: name / jobNumber / groupName / deptName / postName / managerUserName / phone
+     */
+    mapEmployeeInfo(regularization, employee) {
+      const r = regularization || {};
+      const e = employee || {};
+      this.approvalDocumentFileId = pick(r, ['approvalDocumentFileId']);
+      return {
+        userId: r.userId || e.id,
+        // 申请员工
+        userName: r.employeeName || e.name,
+        // 工号
+        jobNumber: r.employeeNo || e.jobNumber,
+        // 手机号
+        mobile: e.phone || e.mobile,
+        // 所属机构
+        groupName: r.groupName || e.groupName,
+        // 所属部门
+        deptName: r.departmentName || r.deptName || e.deptName,
+        // 岗位(模板用 positionName 渲染)
+        positionName: r.positionName || e.postName,
+        // 岗位(兜底)
+        postName: e.postName,
+        // 直属上级
+        managerName: r.managerUserName || e.managerUserName
+      };
+    },
+    /** 后端日期 → yyyy-MM-dd */
+    toDateStr(val) {
+      if (!val) return '';
+      const s = String(val);
+      // 已经是 yyyy-MM-dd / yyyy-MM-dd HH:mm:ss
+      if (/^\d{4}-\d{2}-\d{2}/.test(s)) return s.slice(0, 10);
+      // 毫秒时间戳
+      if (/^\d{10,13}$/.test(s)) {
+        const d = new Date(Number(s));
+        if (!isNaN(d.getTime())) {
+          const pad = n => String(n).padStart(2, '0');
+          return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
+        }
+      }
+      return s.slice(0, 10);
+    },
+    async bootstrap(id) {
+      this.loading = true;
+      try {
+        const regularization = await getRegularizationById(id);
+        this.form = this.mapRegularization(regularization);
+
+        // 用转正单的 userId 补查员工档案,拿到完整的员工信息
+        let employee = null;
+        if (regularization?.userId) {
+          try {
+            employee = await getEmployeeById(regularization.userId);
+            if (process.env.NODE_ENV !== 'production') {
+              // eslint-disable-next-line no-console
+              console.log('[RegularizationDetail] 员工档案 raw employee:', employee);
+              // eslint-disable-next-line no-console
+              console.log('[RegularizationDetail] 转正单 raw keys:', Object.keys(regularization));
+            }
+          } catch (e) {
+            if (process.env.NODE_ENV !== 'production') {
+              console.warn('[RegularizationDetail] 员工档案补查失败:', e);
+            }
+          }
+        }
+        this.employeeInfo = this.mapEmployeeInfo(regularization, employee);
+
+        // 转正单 swagger 只有单个 approvalDocumentFileId,统一转成数组给 fileMain
+        const fileId = this.approvalDocumentFileId;
+        this.approvalFileIds = fileId ? [fileId] : [];
+      } catch (error) {
+        this.form = empty();
+        this.employeeInfo = {};
+        if (process.env.NODE_ENV !== 'production') {
+          console.warn('[RegularizationDetail] 加载转正单失败:', error);
+        }
+      } finally {
+        this.loading = false;
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.regularization-detail {
+  padding: 8px;
+}
+
+.detail-section {
+  margin-bottom: 16px;
+}
+
+.info-card {
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  background: #fff;
+  overflow: hidden;
+
+  &__head {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 10px 16px;
+    font-size: 14px;
+    font-weight: 600;
+    color: #303133;
+    background: #f5f7fa;
+    border-bottom: 1px solid #ebeef5;
+
+    i {
+      font-size: 16px;
+      color: #409eff;
+    }
+  }
+
+  &__code {
+    margin-left: auto;
+    font-size: 12px;
+    font-weight: normal;
+  }
+}
+
+.info-card__body {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px 32px;
+  padding: 16px;
+}
+
+.info-item {
+  display: flex;
+  align-items: center;
+  min-width: 240px;
+
+  &__label {
+    flex-shrink: 0;
+    width: 90px;
+    color: #909399;
+    font-size: 13px;
+    text-align: right;
+    padding-right: 8px;
+  }
+
+  &__value {
+    color: #303133;
+    font-size: 13px;
+    font-weight: 500;
+  }
+
+  &__sub {
+    color: #909399;
+    font-weight: normal;
+    margin-left: 4px;
+  }
+}
+
+.regular-form {
+  padding: 8px 16px 16px;
+
+  .form-row {
+    display: flex;
+    gap: 16px;
+    flex-wrap: wrap;
+    margin-bottom: 4px;
+
+    .el-form-item {
+      flex: 1;
+      min-width: 320px;
+    }
+
+    &--extended {
+      .extended-title {
+        width: 100%;
+        padding: 10px 0 4px;
+        color: #e6a23c;
+        font-size: 13px;
+        font-weight: 600;
+        border-top: 1px dashed #ebeef5;
+        margin-top: 8px;
+
+        i {
+          margin-right: 6px;
+        }
+      }
+    }
+  }
+}
+
+.plain-text {
+  color: #303133;
+  font-size: 14px;
+  line-height: 32px;
+}
+</style>

+ 429 - 0
src/views/bpm/handleTask/components/resignationApplication/resignationApplicationDetail.vue

@@ -0,0 +1,429 @@
+<!-- 离职申请详情 -->
+<template>
+  <main class="resignation-detail" v-loading="loading">
+    <!-- 员工头卡片 -->
+    <section class="detail-section">
+      <div class="header-card">
+        <div class="header-card__avatar">
+          {{ (employeeInfo.userName || '').slice(0, 1) || '?' }}
+        </div>
+        <div class="header-card__meta">
+          <div class="header-card__name">
+            <span class="name-text">{{ employeeInfo.userName || '-' }}</span>
+            <span v-if="employeeInfo.jobNumber" class="name-job">{{ employeeInfo.jobNumber }}</span>
+          </div>
+          <div class="header-card__sub">
+            {{ form.resignationNo || employeeInfo.resignationNo || '-' }}
+          </div>
+        </div>
+        <div class="header-card__status">
+          <el-tag :type="approvalStatusTag" effect="plain" size="medium">
+            {{ approvalStatusText }}
+          </el-tag>
+        </div>
+      </div>
+    </section>
+
+    <!-- 详情信息 -->
+    <section class="detail-section">
+      <div class="info-card info-card--body">
+        <div class="kv-grid">
+          <div class="kv-item">
+            <span class="kv-item__label">所属机构</span>
+            <span class="kv-item__value">{{ employeeInfo.groupName || '-' }}</span>
+          </div>
+          <div class="kv-item">
+            <span class="kv-item__label">所属部门</span>
+            <span class="kv-item__value">{{ employeeInfo.deptName || '-' }}</span>
+          </div>
+          <div class="kv-item">
+            <span class="kv-item__label">岗位名称</span>
+            <span class="kv-item__value">{{ employeeInfo.postName || employeeInfo.positionName || '-' }}</span>
+          </div>
+          <div class="kv-item">
+            <span class="kv-item__label">离职类型</span>
+            <span class="kv-item__value">{{ resignationTypeLabel }}</span>
+          </div>
+          <div class="kv-item">
+            <span class="kv-item__label">申请日期</span>
+            <span class="kv-item__value">{{ form.applyDate || '-' }}</span>
+          </div>
+          <div class="kv-item">
+            <span class="kv-item__label">计划最后工作日</span>
+            <span class="kv-item__value">{{ form.plannedLastWorkDate || '-' }}</span>
+          </div>
+          <div class="kv-item">
+            <span class="kv-item__label">实际生效日期</span>
+            <span class="kv-item__value">{{ form.effectiveDate || '-' }}</span>
+          </div>
+          <div class="kv-item">
+            <span class="kv-item__label">交接状态</span>
+            <span class="kv-item__value">{{ handoverStatusLabel }}</span>
+          </div>
+          <div class="kv-item kv-item--full">
+            <span class="kv-item__label">生效 / 归档</span>
+            <span class="kv-item__value">
+              <el-tag :type="effectiveStatusTag" size="mini">{{ effectiveStatusText }}</el-tag>
+               /
+              <el-tag :type="archiveStatusTag" size="mini">{{ archiveStatusText }}</el-tag>
+            </span>
+          </div>
+          <div class="kv-item kv-item--full">
+            <span class="kv-item__label">申请信息</span>
+            <span class="kv-item__value kv-item__value--textarea">{{ (form.reason || form.userComment) || '-' }}</span>
+          </div>
+        </div>
+      </div>
+    </section>
+  </main>
+</template>
+
+<script>
+import { getResignationById } from '@/api/main';
+import { getEmployeeById } from '@/api/organization';
+
+const empty = () => ({
+  id: '',
+  userId: null,
+  resignationNo: '',
+  applyDate: '',
+  plannedLastWorkDate: '',
+  effectiveDate: '',
+  resignationType: '',
+  reason: '',
+  userComment: '',
+  effectiveStatus: '',
+  archiveStatus: '',
+  handoverStatus: '',
+  approvalStatus: '',
+  approvalDocumentFileId: null
+});
+
+/** 离职类型枚举(swagger 里没写死值,这里按 HR 项目常规约定) */
+const RESIGNATION_TYPE_MAP = {
+  VOLUNTARY: '主动离职',
+  INITIATIVE: '主动离职',
+  PASSIVE: '被动离职',
+  INVOLUNTARY: '被动离职',
+  TERMINATION: '辞退',
+  LAYOFF: '裁员',
+  RESIGN: '主动离职'
+};
+
+const APPROVAL_STATUS_MAP = {
+  DRAFT: { text: '草稿', tag: 'info' },
+  APPROVING: { text: '审批中', tag: 'warning' },
+  APPROVED: { text: '已通过', tag: 'success' },
+  REJECTED: { text: '已驳回', tag: 'danger' },
+  CANCELLED: { text: '已取消', tag: 'info' },
+  CANCELED: { text: '已取消', tag: 'info' },
+  COMPLETED: { text: '已完成', tag: 'success' }
+};
+
+const EFFECTIVE_STATUS_MAP = {
+  PENDING: { text: '未生效', tag: 'info' },
+  ACTIVE: { text: '已生效', tag: 'success' },
+  INVALID: { text: 'INVALID', tag: 'info' },
+  NONE: { text: '未生效', tag: 'info' }
+};
+
+const ARCHIVE_STATUS_MAP = {
+  PENDING: { text: '未归档', tag: 'warning' },
+  ARCHIVED: { text: '已归档', tag: 'success' },
+  NONE: { text: '未归档', tag: 'warning' },
+  NOT_ARCHIVED: { text: '未归档', tag: 'warning' }
+};
+
+const HANDOVER_STATUS_MAP = {
+  NOT_CREATED: { text: '未创建', tag: 'info' },
+  PENDING: { text: '待交接', tag: 'warning' },
+  IN_PROGRESS: { text: '交接中', tag: '' },
+  COMPLETED: { text: '已完成', tag: 'success' },
+  CANCELLED: { text: '已取消', tag: 'info' }
+};
+
+function pick(obj, keys) {
+  if (obj == null || typeof obj !== 'object') return '';
+  for (let i = 0; i < keys.length; i++) {
+    const v = obj[keys[i]];
+    if (v !== undefined && v !== null && v !== '') return v;
+  }
+  return '';
+}
+
+export default {
+  name: 'ResignationApplicationDetail',
+  props: {
+    businessId: {
+      default: ''
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      form: empty(),
+      employeeInfo: {}
+    };
+  },
+  computed: {
+    resignationTypeLabel() {
+      const t = String(this.form.resignationType || '').toUpperCase();
+      return RESIGNATION_TYPE_MAP[t] || this.form.resignationType || '-';
+    },
+    approvalStatusText() {
+      const k = String(this.form.approvalStatus || '').toUpperCase();
+      return APPROVAL_STATUS_MAP[k]?.text || this.form.approvalStatus || '-';
+    },
+    approvalStatusTag() {
+      const k = String(this.form.approvalStatus || '').toUpperCase();
+      return APPROVAL_STATUS_MAP[k]?.tag || 'info';
+    },
+    effectiveStatusText() {
+      const k = String(this.form.effectiveStatus || '').toUpperCase();
+      return EFFECTIVE_STATUS_MAP[k]?.text || this.form.effectiveStatus || '-';
+    },
+    effectiveStatusTag() {
+      const k = String(this.form.effectiveStatus || '').toUpperCase();
+      return EFFECTIVE_STATUS_MAP[k]?.tag || 'info';
+    },
+    archiveStatusText() {
+      const k = String(this.form.archiveStatus || '').toUpperCase();
+      return ARCHIVE_STATUS_MAP[k]?.text || this.form.archiveStatus || '-';
+    },
+    archiveStatusTag() {
+      const k = String(this.form.archiveStatus || '').toUpperCase();
+      return ARCHIVE_STATUS_MAP[k]?.tag || 'warning';
+    },
+    handoverStatusLabel() {
+      const k = String(this.form.handoverStatus || '').toUpperCase();
+      return HANDOVER_STATUS_MAP[k]?.text || this.form.handoverStatus || '-';
+    }
+  },
+  watch: {
+    businessId: {
+      immediate: true,
+      handler(id) {
+        if (id) this.bootstrap(id);
+      }
+    }
+  },
+  methods: {
+    toDateStr(val) {
+      if (!val) return '';
+      const s = String(val);
+      if (/^\d{4}-\d{2}-\d{2}/.test(s)) return s.slice(0, 10);
+      if (/^\d{10,13}$/.test(s)) {
+        const d = new Date(Number(s));
+        if (!isNaN(d.getTime())) {
+          const pad = n => String(n).padStart(2, '0');
+          return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
+        }
+      }
+      return s.slice(0, 10);
+    },
+    mapForm(data) {
+      if (!data || typeof data !== 'object') return empty();
+      return {
+        id: data.id ?? '',
+        userId: pick(data, ['userId', 'employeeId']),
+        resignationNo: pick(data, ['resignationNo']),
+        applyDate: this.toDateStr(pick(data, ['applyDate'])),
+        plannedLastWorkDate: this.toDateStr(pick(data, ['plannedLastWorkDate'])),
+        effectiveDate: this.toDateStr(pick(data, ['effectiveDate'])),
+        resignationType: pick(data, ['resignationType']),
+        reason: pick(data, ['reason']),
+        userComment: pick(data, ['userComment']),
+        effectiveStatus: pick(data, ['effectiveStatus']),
+        archiveStatus: pick(data, ['archiveStatus']),
+        handoverStatus: pick(data, ['handoverStatus']),
+        approvalStatus: pick(data, ['approvalStatus']),
+        approvalDocumentFileId: pick(data, ['approvalDocumentFileId'])
+      };
+    },
+    /** 员工信息:离职单接口优先,缺失则从 getEmployeeById 补
+     *  真实原始字段名对照 HR adaptEmployee:
+     *  - 离职单 raw: employeeName / employeeNo / groupName / departmentName / positionName
+     *  - 员工档案 raw: name / jobNumber / groupName / deptName / postName / managerUserName / phone
+     */
+    mapEmployeeInfo(resignation, employee) {
+      const r = resignation || {};
+      const e = employee || {};
+      return {
+        userId: r.userId || e.id,
+        // 姓名
+        userName: r.employeeName || e.name,
+        // 工号
+        jobNumber: r.employeeNo || e.jobNumber,
+        // 手机号
+        mobile: e.phone || e.mobile,
+        // 所属机构
+        groupName: r.groupName || e.groupName,
+        // 所属部门
+        deptName: r.departmentName || r.deptName || e.deptName,
+        // 岗位(模板用 positionName 渲染)
+        positionName: r.positionName || e.postName,
+        // 岗位(兜底)
+        postName: e.postName,
+        // 离职单号
+        resignationNo: r.resignationNo
+      };
+    },
+    async bootstrap(id) {
+      this.loading = true;
+      try {
+        const resignation = await getResignationById(id);
+        this.form = this.mapForm(resignation);
+
+        // 用离职单的 userId 补查员工档案,拿到完整的员工信息
+        let employee = null;
+        if (resignation?.userId) {
+          try {
+            employee = await getEmployeeById(resignation.userId);
+          } catch (e) {
+            // 员工档案查不到也不影响离职单本身
+            if (process.env.NODE_ENV !== 'production') {
+              console.warn('[ResignationDetail] 员工档案补查失败:', e);
+            }
+          }
+        }
+        this.employeeInfo = this.mapEmployeeInfo(resignation, employee);
+      } catch (error) {
+        this.form = empty();
+        this.employeeInfo = {};
+        if (process.env.NODE_ENV !== 'production') {
+          console.warn('[ResignationDetail] 加载离职单失败:', error);
+        }
+      } finally {
+        this.loading = false;
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.resignation-detail {
+  padding: 0;
+}
+
+.detail-section {
+  margin-bottom: 12px;
+}
+
+/* 顶部头像 + 姓名 + 工号 + 单号 + 状态 */
+.header-card {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  padding: 16px 20px;
+  background: #fff;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+
+  &__avatar {
+    flex-shrink: 0;
+    width: 48px;
+    height: 48px;
+    border-radius: 50%;
+    background: linear-gradient(135deg, #409eff 0%, #1d6fd4 100%);
+    color: #fff;
+    font-size: 20px;
+    font-weight: 600;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    letter-spacing: 0;
+  }
+
+  &__meta {
+    flex: 1;
+    min-width: 0;
+  }
+
+  &__name {
+    display: flex;
+    align-items: baseline;
+    gap: 8px;
+
+    .name-text {
+      font-size: 16px;
+      font-weight: 600;
+      color: #303133;
+    }
+
+    .name-job {
+      font-size: 14px;
+      color: #909399;
+      font-family: Menlo, Monaco, Consolas, monospace;
+    }
+  }
+
+  &__sub {
+    margin-top: 4px;
+    font-size: 14px;
+    color: #c0c4cc;
+    font-family: Menlo, Monaco, Consolas, monospace;
+  }
+
+  &__status {
+    flex-shrink: 0;
+  }
+}
+
+/* 下方信息卡 */
+.info-card {
+  background: #fff;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  padding: 4px 0;
+
+  &--body {
+    padding: 4px 0 16px;
+  }
+}
+
+.kv-grid {
+  display: flex;
+  flex-wrap: wrap;
+  border-top: 1px solid #ebeef5;
+}
+
+.kv-item {
+  width: 50%;
+  display: flex;
+  align-items: flex-start;
+  padding: 12px 20px;
+  border-bottom: 1px solid #f2f3f5;
+  box-sizing: border-box;
+
+  &__label {
+    flex-shrink: 0;
+    width: 120px;
+    color: #909399;
+    font-size: 14px;
+    padding-top: 2px;
+  }
+
+  &__value {
+    flex: 1;
+    color: #303133;
+    font-size: 14px;
+    font-weight: 500;
+    word-break: break-all;
+
+    &--textarea {
+      min-height: 48px;
+      line-height: 22px;
+      white-space: pre-wrap;
+    }
+  }
+
+  &__sep {
+    margin: 0 6px;
+  }
+
+  &--full {
+    width: 100%;
+  }
+}
+</style>

+ 1 - 0
src/views/bpm/handleTask/components/resignationHandoverOrder/resignationHandoverOrderDetail.vue

@@ -0,0 +1 @@
+    <!-- 离职交接单详情 -->

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