ZGC4846 il y a 1 mois
Parent
commit
2344d027a4

+ 98 - 2
src/api/organization/index.js

@@ -8,6 +8,14 @@ function unwrap(response) {
   return Promise.reject(new Error(result.message || "服务调用失败"));
 }
 
+function compact(object) {
+  return Object.keys(object).reduce((result, key) => {
+    const value = object[key];
+    if (value !== undefined && value !== null && value !== "") result[key] = value;
+    return result;
+  }, {});
+}
+
 export async function listOrganizations(params) {
   return unwrap(await request.get("/main/group/getGroupList", { params }));
 }
@@ -23,7 +31,8 @@ export async function getOrganizationCode() {
 export async function addOrganization(data) {
   const response = await request.post("/main/group/save", data);
   if (response.data.code == 0) {
-    return response.data.message;
+    // 返回新增数据(含 id),失败回退 message
+    return response.data.data ?? response.data.message;
   }
   return Promise.reject(new Error(response.data.message || "新增失败"));
 }
@@ -31,11 +40,16 @@ export async function addOrganization(data) {
 export async function updateOrganization(data) {
   const response = await request.put("/main/group/update", data);
   if (response.data.code == 0) {
-    return response.data.message;
+    return response.data.data ?? response.data.message;
   }
   return Promise.reject(new Error(response.data.message || "修改失败"));
 }
 
+// 部门岗位关联:批量修改部门岗位编制(PUT /fm/tmaindepartmentposition/updateBatch)
+export async function updateDepartmentPosition(data) {
+  return unwrap(await request.put("/fm/tmaindepartmentposition/updateBatch", data));
+}
+
 export async function removeOrganization(ids) {
   const response = await request.post("/main/group/delete", ids);
   if (response.data.code == 0) {
@@ -52,6 +66,88 @@ export async function getUserPage(params) {
   );
 }
 
+export async function getEmployeeById(id) {
+  return unwrap(await request.get(`/main/user/getById/${id}`));
+}
+
+// 新增/修改员工(POST /main/user/saveNew)
+export async function saveEmployee(data) {
+  return unwrap(await request.post("/main/user/saveNew", data));
+}
+
+// 员工状态/性别枚举映射
+const EMPLOYEE_STATUS = { 全职: 1, 兼职: 2, 实习: 3, 劳务: 4, 离职: 6 };
+const EMPLOYEE_SEX = { 男: 1, 女: 2, 未知: 0 };
+const TRAIN_STATUS = { 生效: 2, 失效: 0 };
+const REWARD_STATUS = { 生效: 2, 已撤销: 4, 已过期: 3 };
+
+// 员工档案表单 → 后端 UserSaveNewPO(仅映射有对应字段的数据,无映射字段不提交)
+export function serializeEmployee(form = {}) {
+  return compact({
+    id: form.id || undefined,
+    name: form.name || "",
+    jobNumber: form.employeeNo || "",
+    sex: EMPLOYEE_SEX[form.gender] ?? form.gender,
+    birthday: form.birthDate || "",
+    age: Number(form.age) || undefined,
+    idCard: form.idCard || "",
+    ethnic: form.ethnic || "",
+    politicsStatus: form.politicalStatus || "",
+    maritalStatus: form.maritalStatus || "",
+    birthplace: form.nativePlace || "",
+    censusAddress: form.registeredAddress || "",
+    currentAddress: form.currentAddress || "",
+    address: form.workAddress || "",
+    educationBackground: form.education || "",
+    school: form.school || "",
+    phone: form.phone || "",
+    email: form.email || "",
+    wxId: form.wechat || "",
+    status: EMPLOYEE_STATUS[form.status] ?? 1,
+    joinDate: form.hireDate || "",
+    positiveDate: form.regularDate || "",
+    leaveDate: form.leaveInfo?.leaveDate || "",
+    isEnabled: form.enabled ? 1 : 0,
+    openingBank: form.bankAccounts?.[0]?.bankName || "",
+    cardNumber: form.bankAccounts?.[0]?.bankAccount || "",
+    secondLinkName: form.emergencyContact || "",
+    secondLinkPhone: form.emergencyPhone || "",
+    remark: form.remarks || "",
+    groupId: form.groupId || undefined,
+    deptIds: form.deptId ? [Number(form.deptId)] : undefined,
+    deptName: form.deptName || "",
+    postId: form.positionId ? String(form.positionId) : undefined,
+    postName: form.position || "",
+    // 子列表:技能/培训/奖惩(字段对齐后端模型)
+    skills: (form.skills || [])
+      .filter((item) => item.name)
+      .map((item) => ({
+        name: item.name,
+        level: item.level || "",
+        years: Number(item.years) || 0,
+        remark: item.remark || "",
+      })),
+    trainRecords: (form.trainingRecords || [])
+      .filter((item) => item.name)
+      .map((item) => ({
+        title: item.name,
+        desce: item.description || "",
+        startTime: item.startTime || "",
+        endTime: item.endTime || "",
+        status: TRAIN_STATUS[item.status] ?? 2,
+        remark: item.remark || "",
+      })),
+    rewardsPunishments: (form.rewardPunishmentRecords || []).map((item) => ({
+      type: /奖励|表扬|奖金/.test(item.type) ? 1 : 2,
+      desce: item.description || "",
+      startTime: item.startDate || "",
+      endTime: item.endDate || "",
+      status: REWARD_STATUS[item.status] ?? 2,
+      remark: item.remark || "",
+    })),
+  });
+}
+
 export async function exportOrganizations() {
   const response = await request.post(
     "/main/group/exportGroups",

+ 12 - 0
src/styles/views/offerLetterTemplate/index.scss

@@ -269,6 +269,18 @@
       box-shadow: 0 12px 40px rgba(40, 67, 106, 0.14);
       border: 1px solid #e0e7f1;
       border-radius: 4px;
+      /* 自动分页标记 */
+      .page-break-marker {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+        margin: 16px 0;
+        color: #d48806;
+        font-size: 12px;
+        user-select: none;
+        i { flex: 1; height: 1px; border-top: 2px dashed #f0c674; }
+        span { white-space: nowrap; }
+      }
       .canvas-empty {
         display: flex;
         flex-direction: column;

+ 98 - 0
src/styles/views/onboardingContractTemplate/editor.scss

@@ -0,0 +1,98 @@
+:global(.contract-editor-drawer) .el-drawer__body { padding: 0; overflow: hidden; }
+
+.contract-editor {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background: #eef2f8;
+  &.is-fullscreen { position: fixed; inset: 0; z-index: 3000; height: 100vh; }
+
+  .editor-topbar {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 12px 18px;
+    background: #fff;
+    border-bottom: 1px solid #e0e7f1;
+    .editor-title { display: flex; align-items: center; gap: 10px; flex: 1;
+      > span { display: grid; place-items: center; width: 36px; height: 36px; border-radius: 10px; background: rgba(23, 104, 229, 0.12); color: #1768e5; font-size: 18px; }
+      h2 { margin: 0; font-size: 16px; color: #182338; }
+      p { margin: 2px 0 0; font-size: 12px; color: #9aa6b6; }
+    }
+  }
+
+  .editor-main { flex: 1; display: flex; min-height: 0; overflow: hidden; }
+
+  .editor-sidebar {
+    width: 250px; flex-shrink: 0; background: #fff; border-right: 1px solid #e0e7f1;
+    overflow-y: auto; padding: 14px;
+    .sidebar-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; h3 { margin: 0; font-size: 14px; } }
+    .component-group { margin-bottom: 12px;
+      .group-name { font-size: 12px; color: #7c899d; padding: 4px 2px; font-weight: 600; }
+      .component-items { display: flex; flex-direction: column; gap: 8px; }
+      .component-item {
+        display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px;
+        border: 1px dashed #cdd6e4; border-radius: 10px; cursor: grab; transition: all 0.15s; background: #fafcff;
+        &:hover { border-color: #1768e5; background: rgba(23, 104, 229, 0.06); }
+        &:active { cursor: grabbing; }
+        i { font-size: 18px; color: #1768e5; margin-top: 2px; }
+        div { display: flex; flex-direction: column; min-width: 0; }
+        strong { font-size: 13px; color: #182338; }
+        small { font-size: 11px; color: #9aa6b6; margin-top: 2px; line-height: 1.4; }
+      }
+    }
+    .sidebar-tip { display: flex; align-items: flex-start; gap: 6px; padding: 10px; border-radius: 8px; background: #f5f8fd; font-size: 12px; color: #7c899d; }
+  }
+
+  .editor-canvas-wrap { flex: 1; min-width: 0; overflow-y: auto; padding: 16px 20px;
+    .canvas-ruler { text-align: center; font-size: 12px; color: #7c899d; margin-bottom: 10px; }
+    .offer-canvas {
+      width: 780px; margin: 0 auto; min-height: 980px; background: #fff; padding: 48px 52px;
+      box-shadow: 0 12px 40px rgba(40, 67, 106, 0.14); border: 1px solid #e0e7f1; border-radius: 4px;
+      .page-break-marker { display: flex; align-items: center; gap: 10px; margin: 16px 0; color: #d48806; font-size: 12px; user-select: none;
+        i { flex: 1; height: 1px; border-top: 2px dashed #f0c674; } span { white-space: nowrap; } }
+      .canvas-empty { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 120px 0; color: #c2ccd9; i { font-size: 42px; } p { margin: 0; font-size: 13px; } }
+      .canvas-block {
+        position: relative; margin-bottom: 8px; padding: 6px 8px; border: 1px dashed transparent; border-radius: 6px; cursor: pointer; transition: all 0.12s;
+        &:hover { border-color: #b9cbe8; background: rgba(23, 104, 229, 0.04); }
+        &.is-selected { border-color: #1768e5; background: rgba(23, 104, 229, 0.08); }
+        .block-ops { display: none; position: absolute; top: 2px; right: 2px; gap: 4px;
+          i { font-size: 13px; color: #1768e5; padding: 3px; border-radius: 4px; background: #fff; cursor: pointer; } }
+        &:hover .block-ops { display: flex; }
+      }
+    }
+  }
+
+  .editor-props {
+    width: 280px; flex-shrink: 0; background: #fff; border-left: 1px solid #e0e7f1; overflow-y: auto;
+    .props-head { padding: 14px 16px; border-bottom: 1px solid #eef2f7; h3 { margin: 0; font-size: 14px; } small { font-size: 12px; color: #9aa6b6; } }
+    .props-body { padding: 14px 16px; display: flex; flex-direction: column; gap: 14px;
+      .props-item { display: flex; flex-direction: column; gap: 6px; label { font-size: 12px; color: #7c899d; } }
+      .props-row { flex-direction: row; align-items: center; justify-content: space-between; }
+      .props-actions { display: flex; gap: 8px; flex-wrap: wrap; padding-top: 8px; border-top: 1px solid #eef2f7; }
+    }
+    .props-empty { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 80px 20px; color: #c2ccd9; i { font-size: 36px; } p { margin: 0; font-size: 13px; text-align: center; } }
+  }
+
+  .editor-footer {
+    display: flex; align-items: center; gap: 12px; padding: 12px 18px; background: #fff; border-top: 1px solid #e0e7f1;
+    > span { flex: 1; font-size: 12px; color: #7c899d; } > div { display: flex; gap: 8px; }
+  }
+}
+
+/* 渲染类 */
+.render-p { margin: 0; line-height: 1.9; color: #1a1a1a; font-size: 14px; white-space: pre-line; }
+.render-heading { margin: 12px 0; line-height: 1.5; color: #111; font-weight: 700; }
+h1.render-heading { font-size: 26px; letter-spacing: 4px; text-align: center; }
+h2.render-heading { font-size: 20px; }
+h3.render-heading { font-size: 16px; }
+.render-ul { margin: 4px 0; padding-left: 22px; li { line-height: 1.9; color: #1a1a1a; font-size: 14px; } }
+.render-divider { border: none; border-top: 1px solid #d8dee9; margin: 14px 0; }
+.render-signature { margin-top: 24px;
+  .render-p { white-space: pre-line; }
+  .signature-area { display: flex; justify-content: space-between; gap: 40px; margin-top: 36px;
+    .signature-box { display: flex; flex-direction: column; gap: 8px; font-size: 13px; color: #555;
+      strong { font-size: 14px; color: #1a1a1a; } span { color: #7c899d; } } } }
+.align-left { text-align: left; }
+.align-center { text-align: center; }
+.align-right { text-align: right; }

+ 1 - 1
src/views/employeeArchive/components/EmployeeArchiveDetail.vue

@@ -158,7 +158,7 @@
           </div>
         </el-tab-pane>
 
-        <el-tab-pane label="成长与分析" name="growth">
+        <el-tab-pane label="成长与分析(AI)" name="growth">
           <div class="tab-content">
             <section class="insight-panel">
               <div class="insight-heading"><i class="el-icon-data-analysis"></i><div><strong>档案分析摘要</strong><small>基于当前模拟档案规则生成</small></div></div>

+ 408 - 188
src/views/employeeArchive/components/EmployeeArchiveForm.vue

@@ -32,102 +32,83 @@
           </span>
 
           <div v-if="section.name === 'base'" class="section-content">
-            <header class="section-title"><div><h3>基本身份信息</h3><p>员工身份、户籍及个人资料</p></div><span>必填 {{ requiredProgress.base }}</span></header>
-            <section class="profile-section">
-              <div class="avatar-editor">
-                <el-upload action="#" accept="image/*" :show-file-list="false" :before-upload="handleAvatarUpload">
-                  <div class="avatar-preview" :style="avatarStyle">
-                    <img v-if="form.avatar" :src="form.avatar" alt="员工照片" />
-                    <span v-else>{{ nameInitial }}</span>
-                    <div class="avatar-mask"><i class="el-icon-camera"></i></div>
-                  </div>
-                </el-upload>
-                <strong>员工照片</strong><small>JPG / PNG,2MB 以内</small>
-              </div>
-              <el-row :gutter="16" class="profile-fields">
-                <el-col :xs="24" :sm="12"><el-form-item label="员工姓名" prop="name"><el-input v-model.trim="form.name" maxlength="20" placeholder="请输入员工姓名" /></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="员工编号" prop="employeeNo"><el-input v-model.trim="form.employeeNo" maxlength="20" placeholder="请输入员工编号"><el-button slot="append" :disabled="isEdit" @click="generateEmployeeNo">生成</el-button></el-input></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="性别" prop="gender"><el-radio-group v-model="form.gender" class="gender-group"><el-radio-button label="男"><i class="el-icon-male"></i> 男</el-radio-button><el-radio-button label="女"><i class="el-icon-female"></i> 女</el-radio-button></el-radio-group></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="出生日期" prop="birthDate"><el-date-picker v-model="form.birthDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择出生日期" class="full-width" /></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="身份证号" prop="idCard"><el-input v-model.trim="form.idCard" maxlength="18" placeholder="请输入身份证号" /></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="民族"><el-select v-model="form.ethnic" filterable class="full-width"><el-option v-for="item in ethnicOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="政治面貌"><el-select v-model="form.politicalStatus" class="full-width"><el-option v-for="item in politicalOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="婚姻状况"><el-select v-model="form.maritalStatus" class="full-width"><el-option v-for="item in maritalOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="籍贯"><el-input v-model.trim="form.nativePlace" maxlength="30" placeholder="省 / 市" /></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="户口性质"><el-select v-model="form.householdType" clearable class="full-width" placeholder="请选择"><el-option label="城镇" value="城镇" /><el-option label="农村" value="农村" /><el-option label="其他" value="其他" /></el-select></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="健康状况"><el-select v-model="form.healthStatus" class="full-width"><el-option label="优" value="优" /><el-option label="良" value="良" /><el-option label="差" value="差" /></el-select></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="个人证件有效期"><el-date-picker v-model="form.idCardExpiry" value-format="yyyy-MM-dd" type="date" placeholder="请选择有效期" class="full-width" /></el-form-item></el-col>
-              </el-row>
-            </section>
-          </div>
+            <header class="section-title"><div><h3>基本信息</h3><p>员工身份、组织任职与联系方式,字段与档案模型一一对应</p></div><span>必填 {{ requiredProgress.base }}</span></header>
 
-          <div v-else-if="section.name === 'employment'" class="section-content">
-            <header class="section-title"><div><h3>组织与任职信息</h3><p>法人、工厂、部门、岗位及员工状态</p></div><span>必填 {{ requiredProgress.employment }}</span></header>
+            <div class="subsection-title"><strong>身份信息</strong></div>
             <el-row :gutter="16">
-              <el-col :xs="24" :sm="12"><el-form-item label="所属组织" prop="deptId"><el-cascader v-model="form.deptId" :options="organizations" :props="organizationProps" filterable clearable class="full-width" placeholder="请选择公司或部门" @change="handleOrganizationChange" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="所属法人"><el-input v-model.trim="form.legalEntity" maxlength="40" placeholder="请输入所属法人" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="所属工厂"><el-input v-model.trim="form.factory" maxlength="40" placeholder="请输入或选择工厂" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="直属上级"><el-input v-model.trim="form.manager" maxlength="20" placeholder="请输入直属上级" prefix-icon="el-icon-user" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="岗位" prop="position"><el-input v-model.trim="form.position" maxlength="30" placeholder="请输入岗位名称" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="员工性质"><el-select v-model="form.positionType" clearable class="full-width" placeholder="请选择"><el-option v-for="item in ['正式','临时','兼职','编制']" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
-              <el-col :xs="24" :sm="8"><el-form-item label="职级" prop="level"><el-select v-model="form.level" filterable allow-create class="full-width" placeholder="请选择或输入"><el-option v-for="item in levelOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
-              <el-col :xs="24" :sm="8"><el-form-item label="编制级别"><el-select v-model="form.staffingLevel" clearable class="full-width" placeholder="请选择"><el-option v-for="item in staffingLevelOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
-              <el-col :xs="24" :sm="8"><el-form-item label="职责级别"><el-select v-model="form.dutyLevel" clearable class="full-width" placeholder="请选择"><el-option v-for="item in ['初级','中级','高级']" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="用工类型" prop="employmentType"><el-select v-model="form.employmentType" class="full-width"><el-option v-for="item in employmentOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="员工状态" prop="status"><el-select v-model="form.status" class="full-width"><el-option label="在职" value="active" /><el-option label="试用期" value="probation" /><el-option label="已离职" value="inactive" /><el-option label="停用" value="disabled" /></el-select></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="入职日期" prop="hireDate"><el-date-picker v-model="form.hireDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择入职日期" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="转正日期"><el-date-picker v-model="form.regularDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择转正日期" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="系统账号"><el-input v-model.trim="form.account" maxlength="30" placeholder="保存后可开通"><template slot="prepend">@</template></el-input></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="账号状态"><div class="enable-field"><el-switch v-model="form.enabled" active-color="#18a875" /><span>{{ form.enabled ? '启用' : '停用' }}</span></div></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="姓名" prop="name"><el-input v-model.trim="form.name" maxlength="20" placeholder="请输入员工真实姓名" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="性别" prop="gender"><el-select v-model="form.gender" class="full-width"><el-option v-for="item in genderOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="出生日期"><el-date-picker v-model="form.birthDate" value-format="yyyy-MM-dd" type="date" placeholder="可由身份证自动解析" class="full-width" @change="syncAge" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="年龄"><el-input :value="form.age || ''" readonly placeholder="自动计算" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="身份证"><el-input v-model.trim="form.idCard" maxlength="18" placeholder="18位身份证,自动回填出生日期" @blur="parseIdCard" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="民族"><el-select v-model="form.ethnic" filterable allow-create default-first-option class="full-width"><el-option v-for="item in ethnicOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="政治面貌"><el-input v-model.trim="form.politicalStatus" maxlength="20" placeholder="党员、预备党员、群众等" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="婚姻状态"><el-select v-model="form.maritalStatus" class="full-width"><el-option v-for="item in maritalOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="籍贯"><el-input v-model.trim="form.nativePlace" maxlength="30" placeholder="省 / 市" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="最高学历"><el-select v-model="form.education" class="full-width"><el-option v-for="item in educationOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="毕业院校"><el-input v-model.trim="form.school" maxlength="40" placeholder="请输入毕业院校" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="户籍地址"><el-input v-model.trim="form.registeredAddress" maxlength="100" placeholder="请输入户籍地址" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="现居住地址"><el-input v-model.trim="form.currentAddress" maxlength="100" placeholder="请输入现居住地址" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="工作地址"><el-input v-model.trim="form.workAddress" maxlength="60" placeholder="实际办公地点" /></el-form-item></el-col>
+            </el-row>
+
+            <div class="subsection-title"><strong>组织与任职</strong></div>
+            <el-row :gutter="16">
+              <el-col :xs="24" :sm="12"><el-form-item label="工号" prop="employeeNo"><el-input v-model.trim="form.employeeNo" maxlength="20" placeholder="集团全局唯一"><el-button slot="append" :disabled="isEdit" @click="generateEmployeeNo">生成</el-button></el-input></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="用户账号"><el-input v-model.trim="form.account" maxlength="30" placeholder="系统登录账号,支持自动生成、手动修改"><template slot="prepend">@</template></el-input></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="所属工厂"><el-select v-model="form.factory" filterable allow-create default-first-option clearable class="full-width" placeholder="请选择或输入工厂"><el-option v-for="item in factoryOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="所属机构" prop="groupId"><el-select v-model="form.groupId" filterable class="full-width" placeholder="请选择法人组织" @change="handleLegalEntityChange"><el-option v-for="item in legalEntities" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="隶属部门"><el-cascader v-model="form.deptId" :options="deptOptions" :props="organizationProps" filterable clearable class="full-width" placeholder="受所属机构过滤" @change="handleOrganizationChange" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="员工状态" prop="status"><el-select v-model="form.status" class="full-width"><el-option v-for="item in statusOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="入职时间" prop="hireDate"><el-date-picker v-model="form.hireDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择入职日期" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="转正日期"><el-date-picker v-model="form.regularDate" value-format="yyyy-MM-dd" type="date" placeholder="转正生效日期" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="岗位" prop="position"><el-select v-model="form.positionId" filterable clearable class="full-width" placeholder="请选择岗位" @change="handlePositionChange"><el-option v-for="item in positionOptions" :key="item.id" :label="item.positionName + (item.positionSequence ? ' · ' + item.positionSequence : '')" :value="item.id" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="直属上级"><el-input v-model.trim="form.manager" maxlength="20" placeholder="选择系统内员工" prefix-icon="el-icon-user" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="8"><el-form-item label="用工形式"><el-select v-model="form.employmentType" class="full-width"><el-option v-for="item in employmentOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="8"><el-form-item label="编制级别"><el-select v-model="form.staffingLevel" class="full-width"><el-option v-for="item in staffingLevelOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="8"><el-form-item label="职责级别"><el-select v-model="form.dutyLevel" class="full-width"><el-option v-for="item in dutyLevelOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="关键工序"><el-input v-model.trim="form.keyProcess" maxlength="30" placeholder="标记特种工序岗位人员" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="是否启用"><div class="enable-field"><el-switch v-model="form.enabled" active-color="#18a875" /><span>{{ form.enabled ? '启用' : '停用' }}</span></div></el-form-item></el-col>
             </el-row>
-          </div>
 
-          <div v-else-if="section.name === 'contact'" class="section-content">
-            <header class="section-title"><div><h3>联系、地址与家庭</h3><p>日常联络、紧急联系人和家庭成员</p></div><span>必填 {{ requiredProgress.contact }}</span></header>
             <div class="subsection-title"><strong>联系方式</strong></div>
             <el-row :gutter="16">
-              <el-col :xs="24" :sm="12"><el-form-item label="手机号码" prop="phone"><el-input v-model.trim="form.phone" maxlength="11" placeholder="请输入 11 位手机号码" prefix-icon="el-icon-mobile-phone" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="企业邮箱" prop="email"><el-input v-model.trim="form.email" maxlength="50" placeholder="name@company.com" prefix-icon="el-icon-message" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="手机号" prop="phone"><el-input v-model.trim="form.phone" maxlength="11" placeholder="请输入 11 位手机号码" prefix-icon="el-icon-mobile-phone" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="邮箱"><el-input v-model.trim="form.email" maxlength="50" placeholder="name@company.com" prefix-icon="el-icon-message" /></el-form-item></el-col>
               <el-col :xs="24" :sm="12"><el-form-item label="微信号"><el-input v-model.trim="form.wechat" maxlength="30" placeholder="请输入微信号" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="工作地点"><el-input v-model.trim="form.workAddress" maxlength="60" placeholder="请输入工作地点" /></el-form-item></el-col>
-              <el-col :span="24"><el-form-item label="现居住地址"><el-input v-model.trim="form.currentAddress" maxlength="100" placeholder="请输入现居住地址" /></el-form-item></el-col>
-              <el-col :span="24"><el-form-item label="户籍地址"><el-input v-model.trim="form.registeredAddress" maxlength="100" placeholder="请输入户籍地址" /></el-form-item></el-col>
               <el-col :xs="24" :sm="12"><el-form-item label="紧急联系人"><el-input v-model.trim="form.emergencyContact" maxlength="20" placeholder="请输入联系人姓名" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="紧急联系电话" prop="emergencyPhone"><el-input v-model.trim="form.emergencyPhone" maxlength="11" placeholder="请输入联系电话" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="紧急联系电话"><el-input v-model.trim="form.emergencyPhone" maxlength="11" placeholder="请输入联系电话" /></el-form-item></el-col>
             </el-row>
-            <div class="subsection-title"><strong>家庭成员</strong><el-button type="text" icon="el-icon-plus" @click="addFamilyMember">添加成员</el-button></div>
-            <div v-if="form.familyMembers.length" class="repeat-list compact-list">
-              <div v-for="(item, index) in form.familyMembers" :key="`family-${index}`" class="repeat-row family-row">
-                <el-input v-model.trim="item.name" maxlength="20" placeholder="姓名" />
-                <el-select v-model="item.relation" placeholder="关系"><el-option v-for="option in ['配偶','父亲','母亲','子女','其他']" :key="option" :label="option" :value="option" /></el-select>
-                <el-input v-model.trim="item.phone" maxlength="11" placeholder="联系电话" />
-                <el-input v-model.trim="item.workUnit" maxlength="40" placeholder="工作单位" />
-                <el-tooltip content="删除" placement="top"><el-button class="remove-button" icon="el-icon-delete" @click="removeItem('familyMembers', index)"></el-button></el-tooltip>
-              </div>
-            </div>
-            <div v-else class="inline-empty">暂无家庭成员记录</div>
           </div>
 
           <div v-else-if="section.name === 'experience'" class="section-content">
             <header class="section-title"><div><h3>教育与工作履历</h3><p>最高教育经历和入职前工作经历</p></div><span>{{ form.previousJobs.length }} 段履历</span></header>
-            <div class="subsection-title"><strong>教育经历</strong></div>
-            <el-row :gutter="16">
-              <el-col :xs="24" :sm="8"><el-form-item label="最高学历"><el-select v-model="form.education" class="full-width"><el-option v-for="item in educationOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
-              <el-col :xs="24" :sm="8"><el-form-item label="学位"><el-select v-model="form.degree" clearable class="full-width" placeholder="请选择"><el-option v-for="item in ['无','学士','硕士','博士']" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
-              <el-col :xs="24" :sm="8"><el-form-item label="毕业日期"><el-date-picker v-model="form.graduationDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="毕业院校"><el-input v-model.trim="form.school" maxlength="40" placeholder="请输入毕业院校" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="所学专业"><el-input v-model.trim="form.major" maxlength="40" placeholder="请输入所学专业" /></el-form-item></el-col>
-            </el-row>
+            <div class="subsection-title"><strong>教育经历</strong><el-button type="text" icon="el-icon-plus" @click="addEducation">添加教育</el-button></div>
+            <div v-if="form.educations.length" class="repeat-list">
+              <div v-for="(item, index) in form.educations" :key="`edu-${index}`" class="repeat-block">
+                <div class="repeat-block-heading"><strong>教育经历 {{ index + 1 }}</strong><el-button type="text" icon="el-icon-delete" @click="removeItem('educations', index)">删除</el-button></div>
+                <el-row :gutter="14">
+                  <el-col :xs="12" :sm="6"><el-form-item label="最高学历"><el-select v-model="item.education" class="full-width"><el-option v-for="opt in educationOptions" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="学位"><el-select v-model="item.degree" clearable class="full-width" placeholder="请选择"><el-option v-for="opt in ['无','学士','硕士','博士']" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="毕业日期"><el-date-picker v-model="item.graduationDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择" class="full-width" /></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="毕业院校"><el-input v-model.trim="item.school" maxlength="40" placeholder="请输入毕业院校" /></el-form-item></el-col>
+                  <el-col :xs="24" :sm="12"><el-form-item label="所学专业"><el-input v-model.trim="item.major" maxlength="40" placeholder="请输入所学专业" /></el-form-item></el-col>
+                </el-row>
+              </div>
+            </div>
+            <div v-else class="inline-empty">暂无教育经历</div>
             <div class="subsection-title"><strong>工作履历</strong><el-button type="text" icon="el-icon-plus" @click="addPreviousJob">添加履历</el-button></div>
             <div v-if="form.previousJobs.length" class="repeat-list">
               <div v-for="(item, index) in form.previousJobs" :key="`job-${index}`" class="repeat-block">
                 <div class="repeat-block-heading"><strong>工作经历 {{ index + 1 }}</strong><el-button type="text" icon="el-icon-delete" @click="removeItem('previousJobs', index)">删除</el-button></div>
                 <el-row :gutter="14">
-                  <el-col :xs="24" :sm="12"><el-form-item label="起止日期"><el-date-picker v-model="item.dateRange" type="daterange" value-format="yyyy-MM-dd" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" class="full-width" /></el-form-item></el-col>
-                  <el-col :xs="24" :sm="12"><el-form-item label="前公司名称"><el-input v-model.trim="item.company" maxlength="50" placeholder="请输入公司名称" /></el-form-item></el-col>
-                  <el-col :xs="24" :sm="8"><el-form-item label="部门"><el-input v-model.trim="item.department" maxlength="30" /></el-form-item></el-col>
-                  <el-col :xs="24" :sm="8"><el-form-item label="岗位"><el-input v-model.trim="item.position" maxlength="30" /></el-form-item></el-col>
-                  <el-col :xs="24" :sm="8"><el-form-item label="离职原因"><el-input v-model.trim="item.leaveReason" maxlength="40" /></el-form-item></el-col>
+                  <el-col :xs="12" :sm="4"><el-form-item label="序号"><el-input :value="index + 1" readonly /></el-form-item></el-col>
+                  <el-col :xs="12" :sm="5"><el-form-item label="工作开始日期" required><el-date-picker v-model="item.startDate" value-format="yyyy-MM-dd" type="date" placeholder="开始日期" class="full-width" /></el-form-item></el-col>
+                  <el-col :xs="12" :sm="5"><el-form-item label="工作结束日期" required><el-date-picker v-model="item.endDate" value-format="yyyy-MM-dd" type="date" placeholder="结束日期" class="full-width" /></el-form-item></el-col>
+                  <el-col :xs="24" :sm="5"><el-form-item label="前公司名称" required><el-input v-model.trim="item.company" maxlength="50" placeholder="请输入公司名称" /></el-form-item></el-col>
+                  <el-col :xs="24" :sm="5"><el-form-item label="前公司岗位" required><el-input v-model.trim="item.position" maxlength="30" /></el-form-item></el-col>
+                  <el-col :xs="24" :sm="6"><el-form-item label="前公司部门"><el-input v-model.trim="item.department" maxlength="30" /></el-form-item></el-col>
+                  <el-col :xs="24" :sm="6"><el-form-item label="离职原因"><el-input v-model.trim="item.leaveReason" maxlength="40" /></el-form-item></el-col>
                   <el-col :span="24"><el-form-item label="工作描述"><el-input v-model.trim="item.description" type="textarea" :rows="2" maxlength="300" show-word-limit /></el-form-item></el-col>
                 </el-row>
               </div>
@@ -136,54 +117,112 @@
           </div>
 
           <div v-else-if="section.name === 'contract'" class="section-content">
-            <header class="section-title"><div><h3>合同与薪酬银行</h3><p>劳动合同、薪资卡及发薪信息</p></div><span>{{ contractState }}</span></header>
+            <header class="section-title"><div><h3>合同与薪酬银行</h3><p>劳动合同、工资卡信息</p></div><span>{{ contractState }}</span></header>
             <div class="subsection-title"><strong>劳动合同</strong></div>
             <el-row :gutter="16">
-              <el-col :xs="24" :sm="12"><el-form-item label="合同编号"><el-input v-model.trim="form.contract.number" maxlength="30" placeholder="请输入合同编号" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="合同类型"><el-select v-model="form.contract.type" clearable class="full-width" placeholder="请选择"><el-option v-for="item in ['固定期限','无固定期限','以完成一定工作任务为期限','实习协议','劳务协议']" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="签署方式"><el-select v-model="form.contract.signType" clearable class="full-width" placeholder="请选择"><el-option label="线上签署" value="线上签署" /><el-option label="线下签署" value="线下签署" /></el-select></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="试用期(月)"><el-input-number v-model="form.contract.probationMonths" :min="0" :max="6" controls-position="right" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="合同开始日期"><el-date-picker v-model="form.contract.startDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="合同结束日期"><el-date-picker v-model="form.contract.endDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择" class="full-width" /></el-form-item></el-col>
-            </el-row>
-            <div class="subsection-title"><strong>薪酬与银行</strong></div>
-            <el-row :gutter="16">
-              <el-col :xs="24" :sm="12"><el-form-item label="工资开户行"><el-input v-model.trim="form.bankName" maxlength="50" placeholder="请输入银行及支行" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="银行卡号"><el-input v-model.trim="form.bankAccount" maxlength="24" placeholder="请输入工资卡号" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="开户姓名"><el-input v-model.trim="form.bankAccountName" maxlength="20" placeholder="默认与员工姓名一致" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="薪资核算类型"><el-select v-model="form.payrollType" clearable class="full-width" placeholder="请选择"><el-option label="月薪制" value="月薪制" /><el-option label="计时制" value="计时制" /><el-option label="计件制" value="计件制" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="分类"><el-select v-model="form.contract.category" clearable class="full-width" placeholder="请选择"><el-option v-for="item in contractTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="合同名称" required><el-input v-model.trim="form.contract.name" maxlength="60" placeholder="如:2026年度劳动合同" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="8"><el-form-item label="签订时间" required><el-date-picker v-model="form.contract.signDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="8"><el-form-item label="开始时间" required><el-date-picker v-model="form.contract.startDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="8"><el-form-item label="结束时间"><el-date-picker v-model="form.contract.endDate" value-format="yyyy-MM-dd" type="date" placeholder="无固定期限可留空" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="合同附件">
+                <el-upload action="#" :auto-upload="false" :show-file-list="false" multiple :on-change="handleContractFile"><el-button size="small" icon="el-icon-upload2">上传附件</el-button></el-upload>
+                <span v-if="form.contract.files.length" class="file-list inline-file-list"><span v-for="(file, i) in form.contract.files" :key="i"><i class="el-icon-document"></i>{{ file }}<button type="button" @click="form.contract.files.splice(i, 1)"><i class="el-icon-close"></i></button></span></span>
+              </el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="生效状态" required><el-select v-model="form.contract.status" class="full-width"><el-option v-for="item in ['生效','已到期','已解除','已终止']" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
+              <el-col :span="24"><el-form-item label="备注"><el-input v-model.trim="form.contract.remark" type="textarea" :rows="2" maxlength="300" placeholder="续签、变更、解除相关备注" /></el-form-item></el-col>
             </el-row>
+            <div class="subsection-title"><strong>薪酬银行</strong><el-button type="text" icon="el-icon-plus" @click="addBankAccount">添加银行</el-button></div>
+            <div v-if="form.bankAccounts.length" class="repeat-list compact-list">
+              <div v-for="(item, index) in form.bankAccounts" :key="`bank-${index}`" class="repeat-row bank-row">
+                <el-input v-model.trim="item.bankName" maxlength="50" placeholder="开户行" />
+                <el-input v-model.trim="item.bankAccount" maxlength="24" placeholder="银行卡号" />
+                <el-input v-model.trim="item.bankAccountName" maxlength="20" placeholder="姓名(必填)" />
+                <el-tooltip content="删除" placement="top"><el-button class="remove-button" icon="el-icon-delete" @click="removeItem('bankAccounts', index)"></el-button></el-tooltip>
+              </div>
+            </div>
+            <div v-else class="inline-empty">暂无银行账户</div>
           </div>
 
           <div v-else-if="section.name === 'social'" class="section-content">
-            <header class="section-title"><div><h3>社保、资质与附件</h3><p>参保信息、工种、技能证照和电子材料</p></div><span>{{ form.files.length }} 份附件</span></header>
-            <div class="subsection-title"><strong>社保公积金</strong></div>
-            <el-row :gutter="16">
-              <el-col :xs="24" :sm="8"><el-form-item label="社保类型"><el-select v-model="form.socialSecurityType" clearable class="full-width" placeholder="请选择"><el-option v-for="item in socialSecurityOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
-              <el-col :xs="24" :sm="8"><el-form-item label="参保城市"><el-input v-model.trim="form.insuredCity" maxlength="30" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="8"><el-form-item label="参保日期"><el-date-picker v-model="form.insuredDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="8"><el-form-item label="停保日期"><el-date-picker v-model="form.socialSecurityStopDate" value-format="yyyy-MM-dd" type="date" placeholder="未停保" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="8"><el-form-item label="社保基数"><el-input-number v-model="form.socialSecurityBase" :min="0" :precision="2" :controls="false" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="8"><el-form-item label="公积金账号"><el-input v-model.trim="form.housingFundAccount" maxlength="30" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="8"><el-form-item label="公积金基数"><el-input-number v-model="form.housingFundBase" :min="0" :precision="2" :controls="false" class="full-width" /></el-form-item></el-col>
-            </el-row>
-            <div class="subsection-title"><strong>工种与能力</strong></div>
-            <el-row :gutter="16">
-              <el-col :xs="24" :sm="12"><el-form-item label="工种"><el-input v-model.trim="form.workType" maxlength="30" placeholder="如:电工、焊工、装配工" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="特种作业人员"><div class="enable-field"><el-switch v-model="form.specialOperator" active-color="#18a875" /><span>{{ form.specialOperator ? '是' : '否' }}</span></div></el-form-item></el-col>
-              <el-col :span="24"><el-form-item label="技能标签"><el-select v-model="form.skills" multiple filterable allow-create default-first-option class="full-width" placeholder="输入技能后按回车"><el-option v-for="item in skillOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
-            </el-row>
+            <header class="section-title"><div><h3>社保、资质与技能</h3><p>社保公积金、工种、证照与技能列表</p></div><span>{{ form.socialSecurityRecords.length }} 条社保记录</span></header>
+
+            <div class="subsection-title"><strong>社保公积金</strong><el-button type="text" icon="el-icon-plus" @click="addSocialSecurity">添加记录</el-button></div>
+            <div v-if="form.socialSecurityRecords.length" class="repeat-list">
+              <div v-for="(item, index) in form.socialSecurityRecords" :key="`ss-${index}`" class="repeat-block">
+                <div class="repeat-block-heading"><strong>社保记录 {{ index + 1 }}</strong><el-button type="text" icon="el-icon-delete" @click="removeItem('socialSecurityRecords', index)">删除</el-button></div>
+                <el-row :gutter="14">
+                  <el-col :xs="12" :sm="6"><el-form-item label="社保类型"><el-select v-model="item.type" clearable class="full-width"><el-option v-for="opt in socialSecurityOptions" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="基数"><el-input-number v-model="item.base" :min="0" :precision="2" :controls="false" class="full-width" /></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="参保日期"><el-date-picker v-model="item.insuredDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择" class="full-width" /></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="停保日期"><el-date-picker v-model="item.stopDate" value-format="yyyy-MM-dd" type="date" placeholder="在职可为空" class="full-width" /></el-form-item></el-col>
+                  <el-col :xs="24" :sm="8"><el-form-item label="停缴记录"><el-select v-model="item.stopReason" clearable class="full-width" placeholder="请选择"><el-option v-for="opt in ['离职','调转','其他']" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+                </el-row>
+              </div>
+            </div>
+            <div v-else class="inline-empty">暂无社保记录</div>
+
+            <div class="subsection-title"><strong>工种</strong><el-button type="text" icon="el-icon-plus" @click="addWorkType">添加工种</el-button></div>
+            <div v-if="form.workTypes.length" class="repeat-list">
+              <div v-for="(item, index) in form.workTypes" :key="`wt-${index}`" class="repeat-block">
+                <div class="repeat-block-heading"><strong>工种 {{ index + 1 }}</strong><el-button type="text" icon="el-icon-delete" @click="removeItem('workTypes', index)">删除</el-button></div>
+                <el-row :gutter="14">
+                  <el-col :xs="12" :sm="6"><el-form-item label="类型"><el-select v-model="item.type" clearable class="full-width"><el-option v-for="opt in workTypeCategoryOptions" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="编码"><el-input v-model.trim="item.code" maxlength="20" /></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="名称"><el-input v-model.trim="item.name" maxlength="30" /></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="等级"><el-select v-model="item.level" clearable class="full-width"><el-option v-for="opt in workTypeLevelOptions" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="标准工时费"><el-input-number v-model="item.hourlyWage" :min="0" :precision="2" :controls="false" class="full-width" /></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="工时费单位"><el-select v-model="item.wageUnit" clearable class="full-width"><el-option v-for="opt in ['元/小时','元/天','元/件']" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="健康状况"><el-select v-model="item.healthStatus" clearable class="full-width"><el-option v-for="opt in ['优','良','一般','差']" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="是否特种作业人员"><el-select v-model="item.specialOperator" class="full-width"><el-option label="是" value="是" /><el-option label="否" value="否" /></el-select></el-form-item></el-col>
+                  <el-col :span="24"><el-form-item label="证件照">
+                    <el-upload action="#" :auto-upload="false" :show-file-list="false" multiple :on-change="(f) => handleListFile(f, item, 'photo')"><el-button size="small" icon="el-icon-upload2">上传附件</el-button></el-upload>
+                    <span v-if="item.photo && item.photo.length" class="file-list inline-file-list"><span v-for="(file, i) in item.photo" :key="i"><i class="el-icon-document"></i>{{ file }}<button type="button" @click="item.photo.splice(i, 1)"><i class="el-icon-close"></i></button></span></span>
+                  </el-form-item></el-col>
+                </el-row>
+              </div>
+            </div>
+            <div v-else class="inline-empty">暂无工种记录</div>
+
             <div class="subsection-title"><strong>证照列表</strong><el-button type="text" icon="el-icon-plus" @click="addCertificate">添加证照</el-button></div>
-            <div v-if="form.certificates.length" class="repeat-list compact-list">
-              <div v-for="(item, index) in form.certificates" :key="`cert-${index}`" class="repeat-row certificate-row">
-                <el-input v-model.trim="item.name" maxlength="40" placeholder="证照名称" />
-                <el-input v-model.trim="item.number" maxlength="30" placeholder="证照编号" />
-                <el-date-picker v-model="item.issueDate" value-format="yyyy-MM-dd" type="date" placeholder="取得日期" />
-                <el-date-picker v-model="item.expiryDate" value-format="yyyy-MM-dd" type="date" placeholder="到期日期" />
-                <el-tooltip content="删除" placement="top"><el-button class="remove-button" icon="el-icon-delete" @click="removeItem('certificates', index)"></el-button></el-tooltip>
+            <div v-if="form.certificates.length" class="repeat-list">
+              <div v-for="(item, index) in form.certificates" :key="`cert-${index}`" class="repeat-block">
+                <div class="repeat-block-heading"><strong>证照 {{ index + 1 }}</strong><el-button type="text" icon="el-icon-delete" @click="removeItem('certificates', index)">删除</el-button></div>
+                <el-row :gutter="14">
+                  <el-col :xs="12" :sm="6"><el-form-item label="证照类型" required><el-select v-model="item.type" class="full-width"><el-option v-for="opt in certificateTypeOptions" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="证照编号" required><el-input v-model.trim="item.number" maxlength="30" /></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="等级"><el-select v-model="item.level" clearable class="full-width"><el-option v-for="opt in certificateLevelOptions" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="状态" required><el-select v-model="item.status" class="full-width"><el-option label="有效" value="有效" /><el-option label="失效" value="失效" /></el-select></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="有效期开始时间" required><el-date-picker v-model="item.startDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择" class="full-width" /></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="有效期结束时间" required><el-date-picker v-model="item.endDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择" class="full-width" /></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="颁布日期"><el-date-picker v-model="item.issueDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择" class="full-width" /></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="证照附件">
+                    <el-upload action="#" :auto-upload="false" :show-file-list="false" multiple :on-change="(f) => handleListFile(f, item, 'attachment')"><el-button size="small" icon="el-icon-upload2">上传</el-button></el-upload>
+                  </el-form-item></el-col>
+                  <el-col :span="24"><el-form-item label="备注"><el-input v-model.trim="item.remark" maxlength="200" placeholder="补充说明信息" /></el-form-item></el-col>
+                </el-row>
               </div>
             </div>
             <div v-else class="inline-empty">暂无证照记录</div>
+
+            <div class="subsection-title"><strong>技能列表</strong><el-button type="text" icon="el-icon-plus" @click="addSkill">添加技能</el-button></div>
+            <div v-if="form.skills.length" class="repeat-list">
+              <div v-for="(item, index) in form.skills" :key="`skill-${index}`" class="repeat-block">
+                <div class="repeat-block-heading"><strong>技能 {{ index + 1 }}</strong><el-button type="text" icon="el-icon-delete" @click="removeItem('skills', index)">删除</el-button></div>
+                <el-row :gutter="14">
+                  <el-col :xs="12" :sm="6"><el-form-item label="分类"><el-select v-model="item.category" clearable class="full-width"><el-option v-for="opt in skillCategoryOptions" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="技能名称" required><el-input v-model.trim="item.name" maxlength="30" /></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="技能等级"><el-select v-model="item.level" clearable class="full-width"><el-option v-for="opt in skillLevelOptions" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+                  <el-col :xs="12" :sm="6"><el-form-item label="技能经验(年限)"><el-input-number v-model="item.years" :min="0" :precision="1" :controls="false" class="full-width" /></el-form-item></el-col>
+                  <el-col :xs="24" :sm="12"><el-form-item label="证照附件">
+                    <el-upload action="#" :auto-upload="false" :show-file-list="false" multiple :on-change="(f) => handleListFile(f, item, 'attachment')"><el-button size="small" icon="el-icon-upload2">上传</el-button></el-upload>
+                  </el-form-item></el-col>
+                  <el-col :span="24"><el-form-item label="备注"><el-input v-model.trim="item.remark" type="textarea" :rows="2" maxlength="200" placeholder="培训经历、考核情况" /></el-form-item></el-col>
+                </el-row>
+              </div>
+            </div>
+            <div v-else class="inline-empty">暂无技能记录</div>
+
             <div class="subsection-title attachment-title"><strong>档案附件</strong><el-upload action="#" :auto-upload="false" :show-file-list="false" multiple :on-change="handleFileChange"><el-button type="text" icon="el-icon-upload2">上传附件</el-button></el-upload></div>
             <div v-if="form.files.length" class="file-list">
               <span v-for="(file, index) in form.files" :key="`${file}-${index}`"><i class="el-icon-document"></i><strong>{{ file }}</strong><button type="button" aria-label="删除附件" @click="removeItem('files', index)"><i class="el-icon-close"></i></button></span>
@@ -192,7 +231,7 @@
           </div>
 
           <div v-else class="section-content record-section-content">
-            <employee-archive-records :archive="form" />
+            <employee-archive-records ref="records" :archive="form" />
           </div>
         </el-tab-pane>
       </el-tabs>
@@ -207,18 +246,27 @@
 
 <script>
 import EmployeeArchiveRecords from './EmployeeArchiveRecords.vue';
+import { getPositionPage } from '@/api/hr';
+import { listOrganizations } from '@/api/organization';
+import { buildOrganizationTree, flattenOrganizationTree } from '@/utils/organization-tree';
 
-const emptyContract = () => ({ number: '', type: '', signType: '', startDate: '', endDate: '', probationMonths: 3, status: '待签署' });
+const emptyContract = () => ({ category: '', name: '', signDate: '', startDate: '', endDate: '', files: [], status: '生效', remark: '' });
 const emptyForm = () => ({
   id: null, avatar: '', name: '', employeeNo: '', gender: '男', birthDate: '', age: null, idCard: '', idCardExpiry: '', ethnic: '汉族',
   maritalStatus: '未婚', politicalStatus: '群众', nativePlace: '', householdType: '', healthStatus: '优',
-  company: '中盈产业集团有限公司', legalEntity: '中盈产业集团有限公司', factory: '', deptId: '', deptName: '', position: '', positionType: '编制', level: '', staffingLevel: '一级', dutyLevel: '初级', manager: '',
-  employmentType: '全职', status: 'probation', hireDate: '', regularDate: '', account: '', enabled: true,
-  phone: '', email: '', wechat: '', workAddress: '长沙产业园 A 区', currentAddress: '', registeredAddress: '', emergencyContact: '', emergencyPhone: '', familyMembers: [],
-  education: '本科', degree: '学士', school: '', major: '', graduationDate: '', previousJobs: [],
-  contract: emptyContract(), bankName: '', bankAccount: '', bankAccountName: '', payrollType: '月薪制',
-  socialSecurityType: '五险一金', insuredCity: '长沙', insuredDate: '', socialSecurityStopDate: '', socialSecurityBase: 0, housingFundAccount: '', housingFundBase: 0,
-  workType: '', specialOperator: false, skills: [], certificates: [], files: [], remarks: '',
+  company: '中盈产业集团有限公司', legalEntity: '', groupId: null, factory: '', deptId: '', deptName: '', position: '', positionId: null, positionType: '编制', level: '', staffingLevel: '一级', dutyLevel: '初级', manager: '',
+  employmentType: '编制', status: '全职', hireDate: '', regularDate: '', account: '', enabled: true,
+  keyProcess: '',
+  phone: '', email: '', wechat: '', workAddress: '', currentAddress: '', registeredAddress: '', emergencyContact: '', emergencyPhone: '', familyMembers: [],
+  education: '本科', school: '',
+  educations: [],
+  previousJobs: [],
+  contract: emptyContract(), bankAccounts: [],
+  socialSecurityRecords: [],
+  workTypes: [],
+  certificates: [],
+  skills: [],
+  files: [], remarks: '',
   attendanceRecords: [], performanceRecords: [], trainingRecords: [], rewardPunishmentRecords: [], awards: [], positionTransfers: [], contractChanges: [],
   leaveInfo: { enabled: false, position: '', department: '', leaveDate: '', leaveType: '', leaveReason: '', serviceYears: '', handoverStatus: '' }
 });
@@ -247,34 +295,45 @@ export default {
       else callback();
     };
     return {
-      activeTab: 'base', saving: false, form: emptyForm(),
+      activeTab: 'base', saving: false, form: emptyForm(), organizationTree: [],
       sections: [
-        { name: 'base', label: '基本身份', subtitle: '个人与户籍', icon: 'el-icon-user' },
-        { name: 'employment', label: '任职信息', subtitle: '组织与岗位', icon: 'el-icon-office-building' },
-        { name: 'contact', label: '联系与家庭', subtitle: '地址与联系人', icon: 'el-icon-phone-outline' },
+        { name: 'base', label: '基本信息', subtitle: '身份/任职/联系', icon: 'el-icon-user' },
         { name: 'experience', label: '教育与履历', subtitle: '学习与工作经历', icon: 'el-icon-notebook-2' },
         { name: 'contract', label: '合同与薪酬', subtitle: '合同与银行', icon: 'el-icon-document-checked' },
         { name: 'social', label: '社保与资质', subtitle: '证照与附件', icon: 'el-icon-medal' },
         { name: 'records', label: '业务记录', subtitle: '考勤绩效与异动', icon: 'el-icon-tickets' }
       ],
+      // 岗位设定列表(岗位下拉数据源)
+      positionOptions: [],
       ethnicOptions: ['汉族', '土家族', '苗族', '侗族', '回族', '瑶族', '其他'],
-      educationOptions: ['高中/中专', '大专', '本科', '硕士', '博士'],
-      politicalOptions: ['群众', '共青团员', '中共党员', '民主党派', '其他'], maritalOptions: ['未婚', '已婚', '离异', '丧偶', '其他'],
-      employmentOptions: ['全职', '兼职', '实习', '试用', '离职'], levelOptions: ['P1', 'P2', 'P3', 'P4', 'P5', 'M1', 'M2', 'M3', 'M4'],
-      staffingLevelOptions: ['一级', '二级', '三级', '四级', '五级', '六级', '七级', '八级', '九级', '十级'],
+      genderOptions: ['男', '女', '未知'],
+      statusOptions: ['全职', '兼职', '实习', '劳务', '离职'],
+      maritalOptions: ['未婚', '已婚', '离异', '丧偶'],
+      educationOptions: ['小学', '初中', '高中', '大专', '本科', '硕士', '博士'],
+      employmentOptions: ['编制', '外包', '劳务派遣', '临时用工'],
+      staffingLevelOptions: ['一级', '二级', '三级'],
+      dutyLevelOptions: ['初级', '中级', '高级管理'],
+      factoryOptions: ['常德工厂', '长沙工厂', '湘潭工厂', '东莞工厂'],
       socialSecurityOptions: ['四险', '四险一金', '五险', '五险一金', '六险一金'],
-      skillOptions: ['Office 办公', '项目管理', '机械设计', '质量管理', '设备维护', '安全生产'],
+      contractTypeOptions: ['固定期限', '无固定期限', '以完成一定工作任务为期限', '劳务协议', '实习协议'],
+      workTypeCategoryOptions: ['维修', '装配', '检验', '机加', '焊接', '其他'],
+      workTypeLevelOptions: ['初级', '中级', '高级', '技师', '高级技师'],
+      certificateTypeOptions: ['身份证', '护照', '特种作业证', '职称证', '职业资格证', '其他'],
+      certificateLevelOptions: ['初级', '中级', '高级'],
+      skillCategoryOptions: ['专业技能', '办公技能', '语言技能', '特种操作技能', '其他'],
+      skillLevelOptions: ['了解', '熟练', '精通', '初级', '中级', '高级'],
       organizationProps: { value: 'id', label: 'name', children: 'children', emitPath: false, checkStrictly: true },
       rules: {
         name: [{ required: true, message: '请输入员工姓名', trigger: 'blur' }],
-        employeeNo: [{ required: true, message: '请输入员工编号', trigger: 'blur' }, { pattern: /^[A-Za-z0-9_-]{2,20}$/, message: '仅支持字母、数字、下划线和短横线', trigger: 'blur' }, { validator: validateNumber, trigger: 'blur' }],
-        gender: [{ required: true, message: '请选择性别', trigger: 'change' }], birthDate: [{ required: true, message: '请选择出生日期', trigger: 'change' }],
-        idCard: [{ required: true, message: '请输入身份证号', trigger: 'blur' }, { validator: validateIdCard, trigger: 'blur' }],
-        deptId: [{ required: true, message: '请选择所属组织', trigger: 'change' }], position: [{ required: true, message: '请输入岗位名称', trigger: 'blur' }],
-        level: [{ required: true, message: '请选择职级', trigger: 'change' }], employmentType: [{ required: true, message: '请选择用工类型', trigger: 'change' }],
-        status: [{ required: true, message: '请选择员工状态', trigger: 'change' }], hireDate: [{ required: true, message: '请选择入职日期', trigger: 'change' }],
-        phone: [{ required: true, message: '请输入手机号码', trigger: 'blur' }, { validator: validatePhone, trigger: 'blur' }],
-        email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }], emergencyPhone: [{ validator: validatePhone, trigger: 'blur' }]
+        employeeNo: [{ required: true, message: '请输入工号', trigger: 'blur' }, { pattern: /^[A-Za-z0-9_-]{2,20}$/, message: '仅支持字母、数字、下划线和短横线', trigger: 'blur' }, { validator: validateNumber, trigger: 'blur' }],
+        gender: [{ required: true, message: '请选择性别', trigger: 'change' }],
+        groupId: [{ required: true, message: '请选择所属机构', trigger: 'change' }],
+        status: [{ required: true, message: '请选择员工状态', trigger: 'change' }],
+        hireDate: [{ required: true, message: '请选择入职时间', trigger: 'change' }],
+        position: [{ required: true, message: '请选择岗位', trigger: 'change' }],
+        phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }, { validator: validatePhone, trigger: 'blur' }],
+        email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }],
+        idCard: [{ validator: validateIdCard, trigger: 'blur' }]
       }
     };
   },
@@ -284,25 +343,46 @@ export default {
     nameInitial() { return (this.form.name || '新').slice(-1); },
     avatarStyle() { const tones = ['#1768e5', '#13a471', '#7c5ce7', '#d08716', '#d25567']; const code = [...(this.form.name || '新')].reduce((sum, char) => sum + char.charCodeAt(0), 0); return { '--avatar-color': tones[code % tones.length] }; },
     currentSectionLabel() { return this.sections.find((item) => item.name === this.activeTab)?.label || ''; },
+    // 所属机构:法人组织(集团/公司),来源真实机构接口 getGroupList
+    legalEntities() {
+      const tree = this.organizationTree.length ? this.organizationTree : this.organizations;
+      const isLegalEntity = (item) => ['10', '20', 'group', 'company'].includes(String(item.type));
+      return flattenOrganizationTree(tree || []).filter(isLegalEntity);
+    },
+    // 隶属部门:受所属机构过滤的子树
+    deptOptions() {
+      const tree = this.organizationTree.length ? this.organizationTree : this.organizations;
+      if (!this.form.groupId) return tree || [];
+      const find = (nodes) => {
+        for (const node of nodes) {
+          if (String(node.id) === String(this.form.groupId)) return node;
+          const matched = find(node.children || []);
+          if (matched) return matched;
+        }
+        return null;
+      };
+      const selected = find(tree || []);
+      return selected ? [selected] : (tree || []);
+    },
     requiredProgress() {
-      const progress = (keys) => `${keys.filter((key) => this.form[key]).length}/${keys.length}`;
-      return { base: progress(['name', 'employeeNo', 'gender', 'birthDate', 'idCard']), employment: progress(['deptId', 'position', 'level', 'employmentType', 'status', 'hireDate']), contact: progress(['phone']) };
+      const keys = ['name', 'employeeNo', 'gender', 'groupId', 'status', 'hireDate', 'position', 'phone'];
+      return { base: `${keys.filter((key) => this.form[key]).length}/${keys.length}` };
     },
     sectionProgress() {
       const count = (keys) => `${keys.filter((key) => { const value = this.form[key]; return Array.isArray(value) ? value.length : Boolean(value); }).length}/${keys.length}`;
       return {
-        base: count(['name', 'employeeNo', 'gender', 'birthDate', 'idCard', 'ethnic', 'nativePlace', 'householdType']), employment: count(['deptId', 'position', 'level', 'manager', 'hireDate', 'employmentType']),
-        contact: count(['phone', 'email', 'currentAddress', 'emergencyContact']), experience: count(['education', 'degree', 'school', 'major', 'previousJobs']),
+        base: count(['name', 'employeeNo', 'groupId', 'factory', 'deptId', 'status', 'hireDate', 'position', 'employmentType', 'staffingLevel', 'dutyLevel', 'gender', 'birthDate', 'idCard', 'ethnic', 'politicalStatus', 'maritalStatus', 'nativePlace', 'education', 'school', 'phone', 'email', 'wechat', 'currentAddress', 'registeredAddress', 'workAddress', 'emergencyContact', 'emergencyPhone', 'keyProcess']),
+        experience: count(['educations', 'previousJobs']),
         contract: `${[this.form.contract.number, this.form.contract.type, this.form.bankName, this.form.bankAccount].filter(Boolean).length}/4`,
-        social: count(['socialSecurityType', 'insuredDate', 'workType', 'skills', 'certificates', 'files']),
+        social: count(['socialSecurityRecords', 'workTypes', 'certificates', 'skills', 'files']),
         records: `${['attendanceRecords', 'performanceRecords', 'trainingRecords', 'rewardPunishmentRecords', 'awards', 'positionTransfers', 'contractChanges'].reduce((sum, key) => sum + this.form[key].length, 0) + (this.form.leaveInfo.enabled ? 1 : 0)}`
       };
     },
     completionRate() {
-      const values = [this.form.name, this.form.employeeNo, this.form.birthDate, this.form.idCard, this.form.deptId, this.form.position, this.form.level, this.form.hireDate, this.form.phone, this.form.email, this.form.currentAddress, this.form.education, this.form.school, this.form.contract.number, this.form.bankAccount, this.form.insuredDate, this.form.skills.length, this.form.files.length];
+      const values = [this.form.name, this.form.employeeNo, this.form.groupId, this.form.position, this.form.status, this.form.hireDate, this.form.phone, this.form.gender, this.form.email, this.form.birthDate, this.form.education, this.form.school, this.form.currentAddress, this.form.contract.number, this.form.bankAccount, this.form.insuredDate, this.form.skills.length, this.form.files.length];
       return Math.round(values.filter(Boolean).length / values.length * 100);
     },
-    contractState() { return this.form.contract.number ? (this.form.contract.endDate ? '合同资料已填写' : '待补结束日期') : '待录入合同'; }
+    contractState() { return this.form.contract.name ? (this.form.contract.endDate ? '合同资料已填写' : '待补结束日期') : '待录入合同'; }
   },
   watch: { visible(value) { if (value) this.loadForm(); } },
   methods: {
@@ -310,65 +390,205 @@ export default {
       this.activeTab = 'base';
       const source = this.employee ? JSON.parse(JSON.stringify(this.employee)) : {};
       const empty = emptyForm();
+      const statusMap = { active: '全职', probation: '全职', inactive: '离职', disabled: '离职' };
       this.form = {
         ...empty, ...source,
+        groupId: source.groupId ?? source.legalEntityId ?? null,
+        status: statusMap[source.status] || source.status || '全职',
         contract: { ...emptyContract(), ...(source.contract || {}) },
         leaveInfo: { ...empty.leaveInfo, ...(source.leaveInfo || {}) },
-        familyMembers: source.familyMembers || [], previousJobs: source.previousJobs || [], skills: source.skills || [], files: source.files || [],
-        certificates: (source.certificates || []).map((item) => ({ ...item, issueDate: item.issueDate || item.date || '', expiryDate: item.expiryDate || '' })),
+        familyMembers: source.familyMembers || [], files: source.files || [],
+        // 教育经历:兼容旧的单条 education/degree/school 字段
+        educations: source.educations?.length
+          ? source.educations
+          : (source.education
+              ? [{ education: source.education || '本科', degree: source.degree || '', graduationDate: source.graduationDate || '', school: source.school || '', major: source.major || '' }]
+              : []),
+        // 薪酬银行:兼容旧的单条 bankName/bankAccount 字段
+        bankAccounts: source.bankAccounts?.length
+          ? source.bankAccounts
+          : (source.bankName || source.bankAccount
+              ? [{ bankName: source.bankName || '', bankAccount: source.bankAccount || '', bankAccountName: source.bankAccountName || '' }]
+              : []),
+        // 工作履历:兼容旧的 dateRange
+        previousJobs: (source.previousJobs || []).map((item) => ({
+          ...item,
+          startDate: item.startDate || item.dateRange?.[0] || '',
+          endDate: item.endDate || item.dateRange?.[1] || '',
+        })),
+        // 技能:兼容旧字符串数组
+        skills: (source.skills || []).map((item) => (typeof item === 'string' ? { name: item, category: '', level: '', years: 0, attachment: [], remark: '' } : { ...item, attachment: item.attachment || [] })),
+        certificates: (source.certificates || []).map((item) => ({ ...item, attachment: item.attachment || [] })),
+        socialSecurityRecords: source.socialSecurityRecords || [],
+        workTypes: (source.workTypes || []).map((item) => ({ ...item, photo: item.photo || [] })),
         attendanceRecords: source.attendanceRecords || [], performanceRecords: source.performanceRecords || [], trainingRecords: source.trainingRecords || [],
         rewardPunishmentRecords: source.rewardPunishmentRecords || [], awards: source.awards || [], positionTransfers: source.positionTransfers || [], contractChanges: source.contractChanges || []
       };
+      this.loadPositions();
+      this.loadOrganizations();
+      this.syncAge();
       if (!this.employee) this.generateEmployeeNo();
       this.$nextTick(() => this.$refs.form?.clearValidate());
     },
     resetForm() { this.form = emptyForm(); this.activeTab = 'base'; this.saving = false; },
     generateEmployeeNo() { const maxNo = this.existingNumbers.reduce((max, item) => Math.max(max, Number(String(item).match(/\d+/)?.[0] || 0)), 0); this.form.employeeNo = `ZY${String(maxNo + 1).padStart(4, '0')}`; this.$nextTick(() => this.$refs.form?.clearValidate('employeeNo')); },
     handleOrganizationChange(id) {
-      const find = (nodes) => { for (const node of nodes) { if (node.id === id) return node; const result = find(node.children || []); if (result) return result; } return null; };
-      const selected = find(this.organizations); if (selected) { this.form.deptName = selected.name; if (selected.type === 'company' || selected.type === 'group') { this.form.company = selected.name; this.form.legalEntity = selected.name; } }
+      const find = (nodes) => { for (const node of nodes) { if (String(node.id) === String(id)) return node; const result = find(node.children || []); if (result) return result; } return null; };
+      const tree = this.organizationTree.length ? this.organizationTree : this.organizations;
+      const selected = find(tree || []);
+      if (selected) {
+        this.form.deptName = selected.name;
+        if (['10', '20', 'group', 'company'].includes(String(selected.type))) {
+          this.form.company = selected.name;
+          this.form.legalEntity = selected.name;
+          this.form.groupId = selected.id;
+        }
+      }
+    },
+    handleLegalEntityChange(id) {
+      const selected = this.legalEntities.find((item) => String(item.id) === String(id));
+      this.form.legalEntity = selected?.name || '';
+      this.form.company = selected?.name || this.form.company;
+      // 所属机构变化,部门归属随之变化,清空已选部门
+      this.form.deptId = '';
+      this.form.deptName = '';
+    },
+    // 加载真实机构数据(/main/group/getGroupList),用于所属机构与隶属部门
+    async loadOrganizations() {
+      try {
+        const list = await listOrganizations();
+        this.organizationTree = buildOrganizationTree(Array.isArray(list) ? list : []);
+      } catch (error) {
+        this.organizationTree = [];
+      }
+    },
+    handlePositionChange(id) {
+      const selected = this.positionOptions.find((item) => String(item.id) === String(id));
+      this.form.position = selected?.positionName || '';
     },
     handleAvatarUpload(file) { if (file.size > 2 * 1024 * 1024) { this.$message.warning('员工照片不能超过 2MB'); return false; } const reader = new FileReader(); reader.onload = (event) => { this.form.avatar = event.target.result; }; reader.readAsDataURL(file); return false; },
     handleFileChange(file) { if (file.size > 10 * 1024 * 1024) { this.$message.warning('单个附件不能超过 10MB'); return; } if (!this.form.files.includes(file.name)) this.form.files.push(file.name); else this.$message.info('该附件已添加'); },
-    addFamilyMember() { this.form.familyMembers.push({ name: '', relation: '', phone: '', workUnit: '' }); },
-    addPreviousJob() { this.form.previousJobs.push({ dateRange: [], company: '', department: '', position: '', leaveReason: '', description: '' }); },
-    addCertificate() { this.form.certificates.push({ name: '', number: '', issueDate: '', expiryDate: '', status: '有效' }); },
+    addEducation() { this.form.educations.push({ education: '本科', degree: '学士', graduationDate: '', school: '', major: '' }); },
+    addBankAccount() { this.form.bankAccounts.push({ bankName: '', bankAccount: '', bankAccountName: '' }); },
+    addPreviousJob() { this.form.previousJobs.push({ startDate: '', endDate: '', company: '', position: '', department: '', leaveReason: '', description: '' }); },
+    addSocialSecurity() { this.form.socialSecurityRecords.push({ type: '五险一金', base: 0, insuredDate: '', stopDate: '', stopReason: '' }); },
+    addWorkType() { this.form.workTypes.push({ type: '', code: '', name: '', level: '', hourlyWage: 0, wageUnit: '元/小时', healthStatus: '优', specialOperator: '否', photo: [] }); },
+    addCertificate() { this.form.certificates.push({ type: '', number: '', level: '', status: '有效', startDate: '', endDate: '', issueDate: '', attachment: [], remark: '' }); },
+    addSkill() { this.form.skills.push({ category: '', name: '', level: '', years: 0, attachment: [], remark: '' }); },
     removeItem(key, index) { this.form[key].splice(index, 1); },
+    // 列表行内附件上传(把文件名存入对应字段)
+    handleListFile(file, item, key) {
+      if (!Array.isArray(item[key])) this.$set(item, key, []);
+      if (file.name && !item[key].includes(file.name)) item[key].push(file.name);
+    },
+    // 合同附件上传
+    handleContractFile(file) {
+      if (file.name && !this.form.contract.files.includes(file.name)) this.form.contract.files.push(file.name);
+    },
     calculateAge(date) { if (!date) return null; const birthday = new Date(date); const today = new Date(); let age = today.getFullYear() - birthday.getFullYear(); if (today.getMonth() < birthday.getMonth() || (today.getMonth() === birthday.getMonth() && today.getDate() < birthday.getDate())) age -= 1; return age; },
-    firstInvalidTab(fields) { const names = Object.keys(fields || {}); if (names.some((name) => ['name', 'employeeNo', 'gender', 'birthDate', 'idCard'].includes(name))) return 'base'; if (names.some((name) => ['deptId', 'position', 'level', 'employmentType', 'status', 'hireDate'].includes(name))) return 'employment'; return 'contact'; },
-    validateDateLogic() {
-      const checks = [
-        [this.form.birthDate, this.form.hireDate, '入职日期不能早于出生日期', 'employment'],
-        [this.form.hireDate, this.form.regularDate, '转正日期不能早于入职日期', 'employment'],
-        [this.form.contract.startDate, this.form.contract.endDate, '合同结束日期不能早于开始日期', 'contract'],
-        [this.form.insuredDate, this.form.socialSecurityStopDate, '停保日期不能早于参保日期', 'social']
-      ];
-      const invalid = checks.find(([start, end]) => start && end && start > end);
-      if (!invalid) return true;
-      this.activeTab = invalid[3];
-      this.$message.warning(invalid[2]);
-      return false;
+    // 出生日期变化 → 自动计算年龄(只读)
+    syncAge() { this.form.age = this.calculateAge(this.form.birthDate); },
+    // 身份证号 → 自动回填出生日期与年龄
+    parseIdCard() {
+      const value = String(this.form.idCard || '').trim();
+      if (!/^\d{17}[\dXx]$/.test(value)) return;
+      const birth = `${value.slice(6, 10)}-${value.slice(10, 12)}-${value.slice(12, 14)}`;
+      if (!this.form.birthDate) this.form.birthDate = birth;
+      this.form.age = this.calculateAge(birth);
+    },
+    // 岗位设定列表(岗位下拉数据源)
+    async loadPositions() {
+      try {
+        const data = await getPositionPage({ pageNum: 1, size: 1000, status: 1 });
+        this.positionOptions = (data?.list || []).filter((item) => item.positionName);
+      } catch (error) {
+        this.positionOptions = [];
+      }
     },
-    submit() {
-      this.$refs.form.validate((valid, fields) => {
-        if (!valid) { this.activeTab = this.firstInvalidTab(fields); this.$message.warning('请完善标记的必填信息'); return; }
-        if (!this.validateDateLogic()) return;
-        this.saving = true;
-        const today = new Date().toISOString().slice(0, 10);
-        const certificates = this.form.certificates.filter((item) => item.name).map((item) => ({ ...item, date: item.issueDate, status: item.expiryDate && item.expiryDate < new Date().toISOString().slice(0, 10) ? '已到期' : '有效' }));
-        const contractStatus = !this.form.contract.number ? '待签署' : this.form.contract.endDate && this.form.contract.endDate < today ? '已到期' : '生效中';
-        const contract = { ...this.form.contract, status: contractStatus };
-        const leaveEnabled = this.form.leaveInfo.enabled && Boolean(this.form.leaveInfo.leaveDate);
-        const awards = this.form.awards.filter((item) => item.name).map((item) => ({ ...item, level: item.level || '公司级' }));
-        const payload = {
-          ...this.form, certificates, contract, awards,
-          status: leaveEnabled ? 'inactive' : this.form.status,
-          employmentType: leaveEnabled ? '离职' : this.form.employmentType,
-          age: this.calculateAge(this.form.birthDate),
-          bankAccountName: this.form.bankAccountName || this.form.name
-        };
-        setTimeout(() => { this.$emit('save', payload); this.saving = false; this.dialogVisible = false; }, 350);
+    // 保存前整体校验:基础必填 + 各分区必填 + 日期逻辑 + 记录分区,校验不过跳转到对应 tab
+    async validateSections() {
+      // 1. 基础 el-form(base tab 必填)
+      const formValid = await new Promise((resolve) => {
+        this.$refs.form.validate((valid) => resolve(valid));
       });
+      if (!formValid) {
+        this.activeTab = 'base';
+        this.$message.warning('请完善基本信息中标记的必填项');
+        return false;
+      }
+      // 2. 各分区重复列表必填/日期逻辑
+      const sectionInvalid = this.checkRepeatLists();
+      if (sectionInvalid) {
+        this.activeTab = sectionInvalid.tab;
+        this.$message.warning(sectionInvalid.message);
+        return false;
+      }
+      // 3. 记录分区(考勤/绩效/培训/奖惩)
+      const recordsResult = this.$refs.records?.validateRecords?.();
+      if (recordsResult && !recordsResult.valid) {
+        this.activeTab = 'records';
+        this.$message.warning(recordsResult.message);
+        return false;
+      }
+      return true;
+    },
+    // 校验各分区重复列表必填与日期逻辑,返回首个不满足项 { tab, message } 或 null
+    checkRepeatLists() {
+      const gt = (start, end) => start && end && start > end;
+      // 日期逻辑(基本信息)
+      if (gt(this.form.birthDate, this.form.hireDate)) return { tab: 'base', message: '入职日期不能早于出生日期' };
+      if (gt(this.form.hireDate, this.form.regularDate)) return { tab: 'base', message: '转正日期不能早于入职日期' };
+      // 工作履历
+      for (const item of this.form.previousJobs || []) {
+        if (!item.startDate || !item.endDate || !item.company || !item.position) {
+          return { tab: 'experience', message: '工作履历的必填项未完善(工作开始/结束日期、前公司名称、前公司岗位)' };
+        }
+        if (gt(item.startDate, item.endDate)) return { tab: 'experience', message: '工作结束日期必须晚于开始日期' };
+      }
+      // 劳动合同:结束≥开始
+      if (gt(this.form.contract.startDate, this.form.contract.endDate)) return { tab: 'contract', message: '合同结束日期不能早于开始日期' };
+      // 薪酬银行:姓名必填
+      for (const item of this.form.bankAccounts || []) {
+        if (!item.bankAccountName) return { tab: 'contract', message: '薪酬银行的姓名(开户人)必填' };
+      }
+      // 社保记录:停保≥参保
+      for (const item of this.form.socialSecurityRecords || []) {
+        if (gt(item.insuredDate, item.stopDate)) return { tab: 'social', message: '社保停保日期不能早于参保日期' };
+      }
+      // 证照:类型/编号/状态/有效期必填,有效期结束≥开始
+      for (const item of this.form.certificates || []) {
+        if (!item.type || !item.number || !item.status || !item.startDate || !item.endDate) {
+          return { tab: 'social', message: '证照的必填项未完善(证照类型、证照编号、状态、有效期起止)' };
+        }
+        if (gt(item.startDate, item.endDate)) return { tab: 'social', message: '证照有效期结束时间不能早于开始时间' };
+      }
+      // 技能:技能名称必填
+      for (const item of this.form.skills || []) {
+        if (!item.name) return { tab: 'social', message: '技能名称必填' };
+      }
+      return null;
+    },
+    async submit() {
+      const ok = await this.validateSections();
+      if (!ok) return;
+      this.saving = true;
+      const today = new Date().toISOString().slice(0, 10);
+      // 证照:过滤未选类型的行,有效期已过自动置为失效
+      const certificates = this.form.certificates.filter((item) => item.type).map((item) => ({ ...item, status: item.endDate && item.endDate < today ? '失效' : (item.status || '有效') }));
+      const contract = { ...this.form.contract };
+      const leaveEnabled = this.form.leaveInfo.enabled && Boolean(this.form.leaveInfo.leaveDate);
+      const awards = this.form.awards.filter((item) => item.name).map((item) => ({ ...item, level: item.level || '公司级' }));
+      // 银行开户姓名默认与员工姓名一致
+      const bankAccounts = (this.form.bankAccounts || []).map((item, i) => ({
+        ...item,
+        bankAccountName: item.bankAccountName || (i === 0 ? this.form.name : ''),
+      }));
+      const payload = {
+        ...this.form, certificates, contract, awards, bankAccounts,
+        status: leaveEnabled ? '离职' : this.form.status,
+        age: this.calculateAge(this.form.birthDate)
+      };
+      setTimeout(() => { this.$emit('save', payload); this.saving = false; this.dialogVisible = false; }, 350);
     }
   }
 };

+ 87 - 49
src/views/employeeArchive/components/EmployeeArchiveRecords.vue

@@ -14,15 +14,15 @@
           <record-empty v-if="!archive.attendanceRecords.length" text="暂无考勤记录" />
           <record-block v-for="(row, index) in archive.attendanceRecords" :key="`attendance-${index}`" :title="`考勤记录 ${index + 1}`" @remove="removeRecord('attendanceRecords', index)">
             <el-row :gutter="12">
-              <el-col :xs="24" :sm="8"><el-form-item label="考勤月份"><el-date-picker v-model="row.month" type="month" value-format="yyyy-MM" placeholder="请选择月份" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="12" :sm="8"><el-form-item label="应出勤天"><el-input-number v-model="row.requiredDays" :min="0" :max="31" :controls="false" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="12" :sm="8"><el-form-item label="实际出勤天"><el-input-number v-model="row.actualDays" :min="0" :max="31" :controls="false" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="12" :sm="6"><el-form-item label="迟到早退天"><el-input-number v-model="row.lateEarlyDays" :min="0" :max="31" :controls="false" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="12" :sm="6"><el-form-item label="请假分类"><el-select v-model="row.leaveType" clearable class="full-width"><el-option v-for="option in ['事假','病假','年假','调休','婚假','产假','其他']" :key="option" :label="option" :value="option" /></el-select></el-form-item></el-col>
-              <el-col :xs="12" :sm="6"><el-form-item label="请假天"><el-input-number v-model="row.leaveDays" :min="0" :controls="false" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="12" :sm="6"><el-form-item label="出差天"><el-input-number v-model="row.tripDays" :min="0" :controls="false" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="12" :sm="6"><el-form-item label="外出天"><el-input-number v-model="row.outingDays" :min="0" :controls="false" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="12" :sm="6"><el-form-item label="加班天"><el-input-number v-model="row.overtimeDays" :min="0" :controls="false" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="8"><el-form-item label="工号"><el-input :value="archive.employeeNo || ''" readonly /></el-form-item></el-col>
+              <el-col :xs="12" :sm="8"><el-form-item label="应出勤天" required><el-input-number v-model="row.requiredDays" :min="0" :max="31" :precision="1" :controls="false" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="12" :sm="8"><el-form-item label="实际出勤天" required><el-input-number v-model="row.actualDays" :min="0" :max="31" :precision="1" :controls="false" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="12" :sm="6"><el-form-item label="迟到早退天"><el-input-number v-model="row.lateEarlyDays" :min="0" :precision="1" :controls="false" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="12" :sm="6"><el-form-item label="请假分类"><el-select v-model="row.leaveType" clearable class="full-width"><el-option v-for="option in ['事假','病假','年假','婚假','产假','丧假','其他']" :key="option" :label="option" :value="option" /></el-select></el-form-item></el-col>
+              <el-col :xs="12" :sm="6"><el-form-item label="请假天"><el-input-number v-model="row.leaveDays" :min="0" :precision="1" :controls="false" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="12" :sm="6"><el-form-item label="出差天"><el-input-number v-model="row.tripDays" :min="0" :precision="1" :controls="false" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="12" :sm="6"><el-form-item label="外出天"><el-input-number v-model="row.outingDays" :min="0" :precision="1" :controls="false" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="12" :sm="6"><el-form-item label="加班天"><el-input-number v-model="row.overtimeDays" :min="0" :precision="1" :controls="false" class="full-width" /></el-form-item></el-col>
             </el-row>
           </record-block>
         </template>
@@ -31,18 +31,12 @@
           <record-toolbar title="绩效记录" @add="addRecord('performanceRecords')" />
           <record-empty v-if="!archive.performanceRecords.length" text="暂无绩效记录" />
           <record-block v-for="(row, index) in archive.performanceRecords" :key="`performance-${index}`" :title="`绩效记录 ${index + 1}`" @remove="removeRecord('performanceRecords', index)">
-            <el-row :gutter="12" class="performance-row">
-              <el-col :xs="24" :sm="12"><el-form-item label="考核时间范围"><el-date-picker v-model="row.dateRange" type="daterange" value-format="yyyy-MM-dd" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="12" :sm="6"><el-form-item label="考核周期"><el-select v-model="row.cycle" class="full-width"><el-option label="季度" value="季度" /><el-option label="年度" value="年度" /></el-select></el-form-item></el-col>
-              <el-col :xs="12" :sm="6"><el-form-item label="考核日期"><el-date-picker v-model="row.assessmentDate" type="date" value-format="yyyy-MM-dd" placeholder="选择日期" class="full-width" /></el-form-item></el-col>
-            </el-row>
-            <el-row :gutter="12" class="performance-row">
-              <el-col :xs="24" :sm="12"><el-form-item label="绩效考核标准"><el-select v-model="row.standard" filterable allow-create class="full-width"><el-option v-for="option in ['销售额','出勤率','工作效率','完工率','学习','项目进度']" :key="option" :label="option" :value="option" /></el-select></el-form-item></el-col>
-              <el-col :xs="12" :sm="6"><el-form-item label="考核结果"><el-select v-model="row.result" class="full-width"><el-option label="已达标" value="已达标" /><el-option label="未达标" value="未达标" /></el-select></el-form-item></el-col>
-              <el-col :xs="12" :sm="6"><el-form-item label="达标率(%)"><el-input-number v-model="row.achievementRate" :min="0" :max="200" :controls="false" class="full-width" /></el-form-item></el-col>
-            </el-row>
-            <el-row :gutter="12" class="performance-row performance-remark-row">
-              <el-col :span="24"><el-form-item label="考核说明"><el-input v-model.trim="row.remark" type="textarea" :rows="2" maxlength="200" /></el-form-item></el-col>
+            <el-row :gutter="12">
+              <el-col :xs="24" :sm="8"><el-form-item label="考核时间范围" required><el-select v-model="row.cycle" class="full-width"><el-option v-for="opt in ['月度','季度','半年度','年度']" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="8"><el-form-item label="绩效考核标准" required><el-select v-model="row.standard" class="full-width"><el-option v-for="opt in ['销售额','产量','质量','KPI综合','项目进度','出勤率']" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="8"><el-form-item label="考核日期" required><el-date-picker v-model="row.assessmentDate" type="date" value-format="yyyy-MM-dd" placeholder="选择日期" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="12" :sm="6"><el-form-item label="考核结果" required><el-select v-model="row.result" class="full-width"><el-option v-for="opt in ['已达标','未达标','优秀','良好','合格','不合格']" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+              <el-col :xs="12" :sm="6"><el-form-item label="达标率(%)" required><el-input-number v-model="row.achievementRate" :min="0" :max="100" :controls="false" class="full-width" /></el-form-item></el-col>
             </el-row>
           </record-block>
         </template>
@@ -52,12 +46,16 @@
           <record-empty v-if="!archive.trainingRecords.length" text="暂无培训记录" />
           <record-block v-for="(row, index) in archive.trainingRecords" :key="`training-${index}`" :title="`培训记录 ${index + 1}`" @remove="removeRecord('trainingRecords', index)">
             <el-row :gutter="12">
-              <el-col :xs="24" :sm="12"><el-form-item label="培训名称"><el-input v-model.trim="row.name" maxlength="60" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="培训机构"><el-input v-model.trim="row.organization" maxlength="60" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="培训日期"><el-date-picker v-model="row.dateRange" type="daterange" value-format="yyyy-MM-dd" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="12" :sm="6"><el-form-item label="培训学时"><el-input-number v-model="row.hours" :min="0" :controls="false" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="12" :sm="6"><el-form-item label="培训结果"><el-select v-model="row.result" class="full-width"><el-option label="合格" value="合格" /><el-option label="不合格" value="不合格" /><el-option label="完成" value="完成" /></el-select></el-form-item></el-col>
-              <el-col :span="24"><el-form-item label="取得证书"><el-input v-model.trim="row.certificate" maxlength="60" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="分类"><el-select v-model="row.category" clearable class="full-width"><el-option v-for="opt in ['岗前培训','技能培训','安全培训','管理培训','其他']" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="培训标题" required><el-input v-model.trim="row.name" maxlength="60" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="培训说明"><el-input v-model.trim="row.description" type="textarea" :rows="2" maxlength="200" /></el-form-item></el-col>
+              <el-col :xs="12" :sm="6"><el-form-item label="开始时间" required><el-date-picker v-model="row.startTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="开始时间" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="12" :sm="6"><el-form-item label="结束时间" required><el-date-picker v-model="row.endTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="结束时间" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="12" :sm="6"><el-form-item label="生效状态" required><el-select v-model="row.status" class="full-width"><el-option label="生效" value="生效" /><el-option label="失效" value="失效" /></el-select></el-form-item></el-col>
+              <el-col :xs="12" :sm="6"><el-form-item label="附件">
+                <el-upload action="#" :auto-upload="false" :show-file-list="false" multiple :on-change="(f) => handleFile(f, row)"><el-button size="small" icon="el-icon-upload2">上传</el-button></el-upload>
+              </el-form-item></el-col>
+              <el-col :span="24"><el-form-item label="备注"><el-input v-model.trim="row.remark" type="textarea" :rows="2" maxlength="200" placeholder="考核结果、学时等" /></el-form-item></el-col>
             </el-row>
           </record-block>
         </template>
@@ -67,13 +65,15 @@
           <record-empty v-if="!archive.rewardPunishmentRecords.length" text="暂无奖惩记录" />
           <record-block v-for="(row, index) in archive.rewardPunishmentRecords" :key="`reward-${index}`" :title="`奖惩记录 ${index + 1}`" @remove="removeRecord('rewardPunishmentRecords', index)">
             <el-row :gutter="12">
-              <el-col :xs="12" :sm="6"><el-form-item label="奖惩类型"><el-select v-model="row.type" class="full-width"><el-option label="奖励" value="奖励" /><el-option label="处罚" value="处罚" /></el-select></el-form-item></el-col>
-              <el-col :xs="12" :sm="6"><el-form-item label="发生日期"><el-date-picker v-model="row.date" type="date" value-format="yyyy-MM-dd" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="12"><el-form-item label="奖惩名称"><el-input v-model.trim="row.name" maxlength="60" /></el-form-item></el-col>
-              <el-col :xs="12" :sm="8"><el-form-item label="奖项级别"><el-input v-model.trim="row.level" maxlength="30" /></el-form-item></el-col>
-              <el-col :xs="12" :sm="8"><el-form-item label="颁发/处理单位"><el-input v-model.trim="row.issuer" maxlength="50" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="8"><el-form-item label="审批单号"><el-input v-model.trim="row.orderNo" maxlength="40" /></el-form-item></el-col>
-              <el-col :span="24"><el-form-item label="说明"><el-input v-model.trim="row.description" type="textarea" :rows="2" maxlength="200" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="8"><el-form-item label="奖惩类型" required><el-select v-model="row.type" class="full-width"><el-option v-for="opt in ['通报表扬','奖金','记过','警告','其他']" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+              <el-col :xs="12" :sm="6"><el-form-item label="开始时间" required><el-date-picker v-model="row.startDate" type="date" value-format="yyyy-MM-dd" placeholder="开始日期" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="12" :sm="6"><el-form-item label="结束时间" required><el-date-picker v-model="row.endDate" type="date" value-format="yyyy-MM-dd" placeholder="结束日期" class="full-width" /></el-form-item></el-col>
+              <el-col :xs="12" :sm="6"><el-form-item label="生效状态" required><el-select v-model="row.status" class="full-width"><el-option v-for="opt in ['生效','已撤销','已过期']" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+              <el-col :xs="12" :sm="6"><el-form-item label="证照附件">
+                <el-upload action="#" :auto-upload="false" :show-file-list="false" multiple :on-change="(f) => handleFile(f, row)"><el-button size="small" icon="el-icon-upload2">上传</el-button></el-upload>
+              </el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="奖惩说明"><el-input v-model.trim="row.description" type="textarea" :rows="2" maxlength="300" placeholder="事件描述、事由、具体内容" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="12"><el-form-item label="备注"><el-input v-model.trim="row.remark" type="textarea" :rows="2" maxlength="200" placeholder="撤销原因、后续处理" /></el-form-item></el-col>
             </el-row>
           </record-block>
         </template>
@@ -96,14 +96,12 @@
           <record-empty v-if="!archive.positionTransfers.length" text="暂无岗位调动记录" />
           <record-block v-for="(row, index) in archive.positionTransfers" :key="`transfer-${index}`" :title="`岗位调动 ${index + 1}`" @remove="removeRecord('positionTransfers', index)">
             <el-row :gutter="12">
-              <el-col :xs="12" :sm="8"><el-form-item label="调动日期"><el-date-picker v-model="row.date" type="date" value-format="yyyy-MM-dd" class="full-width" /></el-form-item></el-col>
               <el-col :xs="12" :sm="8"><el-form-item label="原岗位"><el-input v-model.trim="row.originalPosition" maxlength="30" /></el-form-item></el-col>
+              <el-col :xs="12" :sm="8"><el-form-item label="所属部门"><el-input v-model.trim="row.originalDepartment" maxlength="30" /></el-form-item></el-col>
+              <el-col :xs="12" :sm="8"><el-form-item label="原办公地"><el-input v-model.trim="row.originalWorkplace" maxlength="60" /></el-form-item></el-col>
               <el-col :xs="12" :sm="8"><el-form-item label="调动岗位"><el-input v-model.trim="row.newPosition" maxlength="30" /></el-form-item></el-col>
-              <el-col :xs="12" :sm="8"><el-form-item label="原部门"><el-input v-model.trim="row.originalDepartment" maxlength="30" /></el-form-item></el-col>
               <el-col :xs="12" :sm="8"><el-form-item label="变更部门"><el-input v-model.trim="row.newDepartment" maxlength="30" /></el-form-item></el-col>
-              <el-col :xs="12" :sm="8"><el-form-item label="审批单号"><el-input v-model.trim="row.orderNo" maxlength="40" /></el-form-item></el-col>
-              <el-col :xs="12" :sm="12"><el-form-item label="原办公地"><el-input v-model.trim="row.originalWorkplace" maxlength="60" /></el-form-item></el-col>
-              <el-col :xs="12" :sm="12"><el-form-item label="现办公地"><el-input v-model.trim="row.newWorkplace" maxlength="60" /></el-form-item></el-col>
+              <el-col :xs="12" :sm="8"><el-form-item label="现办公地"><el-input v-model.trim="row.newWorkplace" maxlength="60" /></el-form-item></el-col>
             </el-row>
           </record-block>
         </template>
@@ -128,10 +126,9 @@
             <el-row :gutter="12">
               <el-col :xs="24" :sm="8"><el-form-item label="岗位"><el-input v-model.trim="archive.leaveInfo.position" maxlength="30" /></el-form-item></el-col>
               <el-col :xs="24" :sm="8"><el-form-item label="所属部门"><el-input v-model.trim="archive.leaveInfo.department" maxlength="30" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="8"><el-form-item label="离职日期"><el-date-picker v-model="archive.leaveInfo.leaveDate" type="date" value-format="yyyy-MM-dd" class="full-width" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="8"><el-form-item label="离职类型"><el-select v-model="archive.leaveInfo.leaveType" clearable class="full-width"><el-option label="个人原因" value="个人原因" /><el-option label="裁员" value="裁员" /><el-option label="调岗" value="调岗" /></el-select></el-form-item></el-col>
-              <el-col :xs="24" :sm="8"><el-form-item label="在职年限"><el-input v-model.trim="archive.leaveInfo.serviceYears" maxlength="20" /></el-form-item></el-col>
-              <el-col :xs="24" :sm="8"><el-form-item label="交接状态"><el-select v-model="archive.leaveInfo.handoverStatus" clearable class="full-width"><el-option label="未开始" value="未开始" /><el-option label="进行中" value="进行中" /><el-option label="已完成" value="已完成" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="8"><el-form-item label="离职日期"><el-date-picker v-model="archive.leaveInfo.leaveDate" type="date" value-format="yyyy-MM-dd" class="full-width" @change="syncServiceYears" /></el-form-item></el-col>
+              <el-col :xs="24" :sm="8"><el-form-item label="离职类型"><el-select v-model="archive.leaveInfo.leaveType" clearable class="full-width"><el-option v-for="opt in ['个人原因','合同到期','辞退','协商解除','其他']" :key="opt" :label="opt" :value="opt" /></el-select></el-form-item></el-col>
+              <el-col :xs="24" :sm="8"><el-form-item label="在职年限"><el-input :value="archive.leaveInfo.serviceYears || ''" readonly placeholder="自动计算" /></el-form-item></el-col>
               <el-col :span="24"><el-form-item label="离职原因"><el-input v-model.trim="archive.leaveInfo.leaveReason" type="textarea" :rows="3" maxlength="300" show-word-limit /></el-form-item></el-col>
             </el-row>
           </div>
@@ -143,12 +140,12 @@
 
 <script>
 const defaults = {
-  attendanceRecords: () => ({ month: '', requiredDays: 0, actualDays: 0, lateEarlyDays: 0, leaveType: '', leaveDays: 0, tripDays: 0, outingDays: 0, overtimeDays: 0 }),
-  performanceRecords: () => ({ dateRange: [], cycle: '季度', result: '已达标', achievementRate: 100, assessmentDate: '', standard: '销售额', remark: '' }),
-  trainingRecords: () => ({ name: '', organization: '', dateRange: [], hours: 0, result: '完成', certificate: '' }),
-  rewardPunishmentRecords: () => ({ type: '奖励', date: '', name: '', level: '', issuer: '', orderNo: '', description: '' }),
+  attendanceRecords: () => ({ requiredDays: 0, actualDays: 0, lateEarlyDays: 0, leaveType: '', leaveDays: 0, tripDays: 0, outingDays: 0, overtimeDays: 0 }),
+  performanceRecords: () => ({ cycle: '月度', standard: '销售额', assessmentDate: '', result: '已达标', achievementRate: 100 }),
+  trainingRecords: () => ({ category: '', name: '', description: '', startTime: '', endTime: '', status: '生效', attachment: [], remark: '' }),
+  rewardPunishmentRecords: () => ({ type: '通报表扬', startDate: '', endDate: '', status: '生效', attachment: [], description: '', remark: '' }),
   awards: () => ({ name: '', date: '', level: '', issuer: '' }),
-  positionTransfers: () => ({ date: '', originalPosition: '', newPosition: '', originalDepartment: '', newDepartment: '', originalWorkplace: '', newWorkplace: '', orderNo: '' }),
+  positionTransfers: () => ({ originalPosition: '', originalDepartment: '', originalWorkplace: '', newPosition: '', newDepartment: '', newWorkplace: '' }),
   contractChanges: () => ({ effectiveDate: '', changeType: '续签', before: '', after: '', orderNo: '' })
 };
 
@@ -198,7 +195,48 @@ export default {
   methods: {
     recordCount(key) { return key === 'leaveInfo' ? (this.archive.leaveInfo?.enabled ? 1 : 0) : (this.archive[key]?.length || 0); },
     addRecord(key) { this.archive[key].push(defaults[key]()); },
-    removeRecord(key, index) { this.archive[key].splice(index, 1); }
+    removeRecord(key, index) { this.archive[key].splice(index, 1); },
+    // 记录行内附件:把文件名存入该行的 attachment
+    handleFile(file, row) {
+      if (!Array.isArray(row.attachment)) this.$set(row, 'attachment', []);
+      if (file.name && !row.attachment.includes(file.name)) row.attachment.push(file.name);
+    },
+    // 在职年限自动计算(入职日期 → 离职日期)
+    syncServiceYears() {
+      const hire = this.archive?.hireDate;
+      const leave = this.archive?.leaveInfo?.leaveDate;
+      if (!hire || !leave) return;
+      const years = Math.floor((new Date(leave) - new Date(hire)) / (365.25 * 24 * 3600 * 1000));
+      this.archive.leaveInfo.serviceYears = years >= 0 ? `${years} 年` : '';
+    },
+    // 保存前校验记录分区必填项;返回 { valid, message }
+    validateRecords() {
+      const label = { attendanceRecords: '考勤记录', performanceRecords: '绩效记录', trainingRecords: '培训记录', rewardPunishmentRecords: '奖惩记录' };
+      const checks = [
+        ['attendanceRecords', ['requiredDays', 'actualDays'], '应出勤天、实际出勤天'],
+        ['performanceRecords', ['cycle', 'standard', 'assessmentDate', 'result', 'achievementRate'], '考核时间范围、绩效考核标准、考核日期、考核结果、达标率'],
+        ['trainingRecords', ['name', 'startTime', 'endTime', 'status'], '培训标题、开始/结束时间、生效状态'],
+        ['rewardPunishmentRecords', ['type', 'startDate', 'endDate', 'status'], '奖惩类型、开始/结束时间、生效状态'],
+      ];
+      for (const [key, fields, names] of checks) {
+        for (const row of this.archive[key] || []) {
+          for (const field of fields) {
+            if (row[field] === '' || row[field] === null || row[field] === undefined) {
+              return { valid: false, message: `${label[key]}的必填项未完善(${names})` };
+            }
+          }
+          // 培训结束≥开始
+          if (key === 'trainingRecords' && row.startTime && row.endTime && row.endTime < row.startTime) {
+            return { valid: false, message: '培训结束时间不能早于开始时间' };
+          }
+          // 奖惩结束≥开始
+          if (key === 'rewardPunishmentRecords' && row.startDate && row.endDate && row.endDate < row.startDate) {
+            return { valid: false, message: '奖惩结束时间不能早于开始时间' };
+          }
+        }
+      }
+      return { valid: true };
+    }
   }
 };
 </script>

+ 10 - 1
src/views/employeeArchive/index.vue

@@ -192,6 +192,7 @@ import {
   collectOrganizationIds,
   flattenOrganizations
 } from './mock';
+import { saveEmployee as saveEmployeeApi, serializeEmployee } from '@/api/organization';
 
 const EMPLOYEE_STORAGE_KEY = 'oa-pc-employee-archive-v2';
 const emptyLeaveInfo = () => ({ enabled: false, position: '', department: '', leaveDate: '', leaveType: '', leaveReason: '', serviceYears: '', handoverStatus: '' });
@@ -342,7 +343,15 @@ export default {
     openEdit(employee) { this.editingEmployee = JSON.parse(JSON.stringify(employee)); this.formVisible = true; },
     editFromDetail(employee) { this.detailVisible = false; this.$nextTick(() => this.openEdit(employee)); },
     openDetail(employee) { this.selectedEmployee = employee; this.detailVisible = true; },
-    saveEmployee(payload) {
+    async saveEmployee(payload) {
+      // 对接后端:POST /main/user/saveNew(新增/修改员工)
+      try {
+        const apiPayload = serializeEmployee(payload);
+        await saveEmployeeApi(apiPayload);
+      } catch (error) {
+        this.$message.error(error.message || '员工保存失败');
+        return;
+      }
       if (payload.id) {
         const index = this.employees.findIndex((item) => item.id === payload.id);
         if (index > -1) {

+ 155 - 98
src/views/offerLetterTemplate/components/OfferTemplateEditor.vue

@@ -61,58 +61,66 @@
           </div>
         </aside>
 
-        <!-- 中:A4 画布 -->
+        <!-- 中:A4 画布(分页) -->
         <section class="editor-canvas-wrap">
-          <div class="canvas-ruler">A4 画布 · 所见即所得 · 【xxx】为动态变量占位符</div>
+          <div class="canvas-ruler">
+            A4 画布 · 所见即所得 · 【xxx】为动态变量占位符 · 虚线为自动分页(共 {{ pageCount }} 页)
+          </div>
           <div
+            ref="canvas"
             class="offer-canvas"
             @dragover.prevent
             @drop="dropComponent($event)"
           >
-            <div
-              v-for="(block, index) in blocks"
-              :key="block.id"
-              class="canvas-block"
-              :class="['type-' + block.type, { 'is-selected': selectedIndex === index }]"
-              draggable="true"
-              @click="selectBlock(index)"
-              @dragstart="dragBlock($event, index)"
-              @dragover.prevent
-              @drop="dropBlock($event, index)"
-            >
-              <div class="block-render">
-                <!-- 标题 -->
-                <component
-                  v-if="block.type === 'heading'"
-                  :is="block.props.size || 'h2'"
-                  class="render-heading"
-                  :class="renderClass(block)"
-                >{{ block.content }}</component>
-                <!-- 项目符号清单 -->
-                <div v-else-if="block.type === 'bullet'">
-                  <p v-if="bulletLines(block)[0]" class="render-p" :class="renderClass(block)">{{ bulletLines(block)[0] }}</p>
-                  <ul v-if="bulletLines(block).length > 1" class="render-ul" :class="renderClass(block)">
-                    <li v-for="(line, li) in bulletLines(block).slice(1)" :key="li">{{ line }}</li>
-                  </ul>
-                </div>
-                <!-- 分割线 -->
-                <hr v-else-if="block.type === 'divider'" class="render-divider" />
-                <!-- 落款签名栏 -->
-                <div v-else-if="block.type === 'signature'" class="render-signature" :class="renderClass(block)">
-                  <p class="render-p">{{ block.content }}</p>
-                  <div class="signature-area">
-                    <span>HR 签署</span><span>公司盖章</span>
+            <template v-for="(block, index) in blocks">
+              <div
+                :key="block.id"
+                class="canvas-block"
+                :class="['type-' + block.type, { 'is-selected': selectedIndex === index }]"
+                draggable="true"
+                @click="selectBlock(index)"
+                @dragstart="dragBlock($event, index)"
+                @dragover.prevent
+                @drop="dropBlock($event, index)"
+              >
+                <div class="block-render">
+                  <!-- 标题 -->
+                  <component
+                    v-if="block.type === 'heading'"
+                    :is="block.props.size || 'h2'"
+                    class="render-heading"
+                    :class="renderClass(block)"
+                  >{{ block.content }}</component>
+                  <!-- 项目符号清单 -->
+                  <div v-else-if="block.type === 'bullet'">
+                    <p v-if="bulletLines(block)[0]" class="render-p" :class="renderClass(block)">{{ bulletLines(block)[0] }}</p>
+                    <ul v-if="bulletLines(block).length > 1" class="render-ul" :class="renderClass(block)">
+                      <li v-for="(line, li) in bulletLines(block).slice(1)" :key="li">{{ line }}</li>
+                    </ul>
+                  </div>
+                  <!-- 分割线 -->
+                  <hr v-else-if="block.type === 'divider'" class="render-divider" />
+                  <!-- 落款签名栏 -->
+                  <div v-else-if="block.type === 'signature'" class="render-signature" :class="renderClass(block)">
+                    <p class="render-p">{{ block.content }}</p>
+                    <div class="signature-area">
+                      <span>HR 签署</span><span>公司盖章</span>
+                    </div>
                   </div>
+                  <!-- 文本/变量/日期/金额 -->
+                  <p v-else class="render-p" :class="renderClass(block)">{{ block.content }}</p>
+                </div>
+                <div class="block-ops">
+                  <i class="el-icon-top" @click.stop="moveBlock(index, -1)"></i>
+                  <i class="el-icon-bottom" @click.stop="moveBlock(index, 1)"></i>
+                  <i class="el-icon-delete" @click.stop="removeBlock(index)"></i>
                 </div>
-                <!-- 文本/变量/日期/金额 -->
-                <p v-else class="render-p" :class="renderClass(block)">{{ block.content }}</p>
               </div>
-              <div class="block-ops">
-                <i class="el-icon-top" @click.stop="moveBlock(index, -1)"></i>
-                <i class="el-icon-bottom" @click.stop="moveBlock(index, 1)"></i>
-                <i class="el-icon-delete" @click.stop="removeBlock(index)"></i>
+              <!-- 自动分页标记 -->
+              <div v-if="pageBreaks.has(index)" :key="`break-${block.id}`" class="page-break-marker">
+                <i></i><span>第 {{ pageIndexOf(index) }} 页</span><i></i>
               </div>
-            </div>
+            </template>
             <div v-if="!blocks.length" class="canvas-empty">
               <i class="el-icon-s-claim"></i>
               <p>画布为空,从左侧拖入组件开始设计</p>
@@ -181,6 +189,7 @@
                 <el-button size="small" icon="el-icon-top" @click="moveBlock(selectedIndex, -1)">上移</el-button>
                 <el-button size="small" icon="el-icon-bottom" @click="moveBlock(selectedIndex, 1)">下移</el-button>
                 <el-button size="small" type="danger" plain icon="el-icon-delete" @click="removeBlock(selectedIndex)">删除</el-button>
+                <el-button size="small" type="primary" plain icon="el-icon-check" @click="saveBlockProps">保存模板(局部)</el-button>
               </div>
             </div>
           </template>
@@ -201,46 +210,39 @@
         </div>
       </div>
 
-      <!-- 预览打印弹窗(用模拟真实业务数据渲染) -->
-      <el-dialog
-        title="Offer 预览"
-        :visible.sync="previewVisible"
-        width="860px"
-        custom-class="offer-preview-dialog"
-        append-to-body
-        :close-on-click-modal="false"
-      >
-        <div class="offer-paper preview-paper" ref="paper">
+      <!-- 打印专用容器:仅在打印时显示,按分页逐页输出 -->
+      <div class="print-offer">
+        <div
+          v-for="(page, pIndex) in paginate()"
+          :key="`print-page-${pIndex}`"
+          class="print-page"
+        >
           <div
-            v-for="(block, index) in blocks"
-            :key="block.id"
-            class="preview-block"
+            v-for="(blockIndex) in page"
+            :key="blocks[blockIndex].id"
+            class="print-block"
           >
             <component
-              v-if="block.type === 'heading'"
-              :is="block.props.size || 'h2'"
+              v-if="blocks[blockIndex].type === 'heading'"
+              :is="blocks[blockIndex].props.size || 'h2'"
               class="render-heading"
-              :class="renderClass(block)"
-            >{{ resolveText(block) }}</component>
-            <div v-else-if="block.type === 'bullet'">
-              <p v-if="bulletLines(block)[0]" class="render-p" :class="renderClass(block)">{{ resolveText({ ...block, content: bulletLines(block)[0] }) }}</p>
-              <ul v-if="bulletLines(block).length > 1" class="render-ul" :class="renderClass(block)">
-                <li v-for="(line, li) in bulletLines(block).slice(1)" :key="li">{{ resolveText({ ...block, content: line }) }}</li>
+              :class="renderClass(blocks[blockIndex])"
+            >{{ resolveText(blocks[blockIndex]) }}</component>
+            <div v-else-if="blocks[blockIndex].type === 'bullet'">
+              <p v-if="bulletLines(blocks[blockIndex])[0]" class="render-p" :class="renderClass(blocks[blockIndex])">{{ resolveText({ ...blocks[blockIndex], content: bulletLines(blocks[blockIndex])[0] }) }}</p>
+              <ul v-if="bulletLines(blocks[blockIndex]).length > 1" class="render-ul" :class="renderClass(blocks[blockIndex])">
+                <li v-for="(line, li) in bulletLines(blocks[blockIndex]).slice(1)" :key="li">{{ resolveText({ ...blocks[blockIndex], content: line }) }}</li>
               </ul>
             </div>
-            <hr v-else-if="block.type === 'divider'" class="render-divider" />
-            <div v-else-if="block.type === 'signature'" class="render-signature" :class="renderClass(block)">
-              <p class="render-p">{{ resolveText(block) }}</p>
+            <hr v-else-if="blocks[blockIndex].type === 'divider'" class="render-divider" />
+            <div v-else-if="blocks[blockIndex].type === 'signature'" class="render-signature" :class="renderClass(blocks[blockIndex])">
+              <p class="render-p">{{ resolveText(blocks[blockIndex]) }}</p>
               <div class="signature-area"><span>HR 签署</span><span>公司盖章</span></div>
             </div>
-            <p v-else class="render-p" :class="renderClass(block)">{{ resolveText(block) }}</p>
+            <p v-else class="render-p" :class="renderClass(blocks[blockIndex])">{{ resolveText(blocks[blockIndex]) }}</p>
           </div>
         </div>
-        <div slot="footer">
-          <el-button @click="previewVisible = false">关闭</el-button>
-          <el-button type="primary" icon="el-icon-printer" @click="printPreview">打印 / 导出 PDF</el-button>
-        </div>
-      </el-dialog>
+      </div>
     </div>
   </el-drawer>
 </template>
@@ -271,13 +273,14 @@ export default {
       collapsed: false,
       blocks: [],
       selectedIndex: -1,
+      // 分页:各组件块实测高度
+      blockHeights: {},
       componentGroups: templateComponentGroups,
       offerVariables,
       textStyleOptions,
       alignOptions,
       headingSizeOptions,
       insertVar: "",
-      previewVisible: false,
       previewData: sampleDataByLabel(),
       previewing: false,
       isNew: true,
@@ -290,11 +293,29 @@ export default {
     canBindVariable() {
       return this.selectedBlock && VARIABLE_TYPES.includes(this.selectedBlock.type);
     },
+    // 分页标记:每页末尾块的索引(用于画布插入分页符)
+    pageBreaks() {
+      const breaks = new Set();
+      const pages = this.paginate();
+      for (let p = 0; p < pages.length - 1; p++) {
+        breaks.add(pages[p][pages[p].length - 1]);
+      }
+      return breaks;
+    },
+    pageCount() {
+      return this.paginate().length;
+    },
   },
   watch: {
     visible(val) {
       if (val) this.initEditor();
     },
+    blocks: {
+      deep: true,
+      handler() {
+        this.measureBlocks();
+      },
+    },
   },
   methods: {
     initEditor() {
@@ -303,12 +324,54 @@ export default {
         JSON.stringify(this.template?.blocks?.length ? this.template.blocks : createDefaultBlocks()),
       );
       this.selectedIndex = -1;
+      this.measureBlocks();
     },
     handleClosed() {
-      this.previewVisible = false;
       this.previewing = false;
       this.selectedIndex = -1;
     },
+    // ===== 分页:按各组件块实测高度把 blocks 分组到各页 =====
+    measureBlocks() {
+      this.$nextTick(() => {
+        const el = this.$refs.canvas;
+        if (!el) return;
+        const nodes = el.querySelectorAll(".canvas-block");
+        const heights = {};
+        nodes.forEach((node, index) => {
+          const id = this.blocks[index]?.id;
+          if (id) heights[id] = Math.ceil(node.offsetHeight || 0);
+        });
+        this.blockHeights = heights;
+      });
+    },
+    paginate() {
+      const PAGE_HEIGHT = 980; // A4 内容区可用高度(px,@96dpi 约 1016,留安全边距)
+      const GAP = 16;
+      const pages = [];
+      let current = [];
+      let used = 24;
+      this.blocks.forEach((block, index) => {
+        const height = this.blockHeights[block.id] || 60;
+        if (current.length && used + height + GAP > PAGE_HEIGHT) {
+          pages.push(current.slice());
+          current = [index];
+          used = height;
+        } else {
+          current.push(index);
+          used += height + GAP;
+        }
+      });
+      if (current.length) pages.push(current);
+      return pages;
+    },
+    // 某块属于第几页(1 起)
+    pageIndexOf(index) {
+      const pages = this.paginate();
+      for (let p = 0; p < pages.length; p++) {
+        if (pages[p].includes(index)) return p + 1;
+      }
+      return 1;
+    },
     // ===== 组件库拖拽 / 添加 =====
     dragComponent(event, item) {
       event.dataTransfer.setData("text/plain", JSON.stringify({ type: item.type, variable: item.defaultVariable || "" }));
@@ -403,14 +466,18 @@ export default {
         .catch(() => {});
     },
     // ===== 预览打印 =====
+    // 直接启动浏览器打印(打印预览页),打印容器按内容自动分页
     openPreview() {
+      if (!this.blocks.length) return this.$message.warning("画布内容为空,请先添加组件");
       this.previewing = true;
-      this.previewVisible = true;
-    },
-    printPreview() {
       this.$nextTick(() => window.print());
     },
     // ===== 保存 =====
+    // 局部保存:确认当前选中组件的属性修改(属性为实时绑定,此处给出保存反馈)
+    saveBlockProps() {
+      if (!this.selectedBlock) return this.$message.warning("请先在画布中选中组件");
+      this.$message.success(`已保存组件「${this.componentLabel(this.selectedBlock.type)}」的属性配置`);
+    },
     saveTemplate() {
       if (!this.blocks.length) return this.$message.warning("画布内容为空,请先添加组件");
       this.$emit("save", {
@@ -424,35 +491,25 @@ export default {
 
 <style lang="scss" scoped src="../../../styles/views/offerLetterTemplate/index.scss"></style>
 <style lang="scss">
-/* 打印 Offer 纸面:隐藏编辑器与弹窗外壳,仅输出 .offer-paper */
+/* 打印 Offer:点击「打印 / 导出 PDF」直接进入浏览器打印,按分页逐页输出,每页一屏 A4 */
+.print-offer { display: none; }
 @media print {
+  @page { size: A4; margin: 14mm 12mm; }
   body * { visibility: hidden !important; }
-  .offer-editor-drawer,
-  .offer-preview-dialog .el-dialog__wrapper {
-    position: static !important;
-    overflow: visible !important;
-    height: auto !important;
-  }
-  .offer-preview-dialog .el-dialog {
-    position: static !important;
-    box-shadow: none !important;
-    width: 100% !important;
-    margin: 0 !important;
-  }
-  .offer-preview-dialog .el-dialog__header,
-  .offer-preview-dialog .preview-toolbar,
   .offer-editor-drawer .editor-topbar,
   .offer-editor-drawer .editor-main,
   .offer-editor-drawer .editor-footer {
     display: none !important;
   }
-  .offer-preview-dialog .offer-paper,
-  .offer-preview-dialog .offer-paper * {
+  .print-offer {
+    display: block !important;
+    width: 100%;
     visibility: visible !important;
   }
-  .offer-preview-dialog .offer-paper {
-    border: none;
-    padding: 0;
-  }
+  .print-offer * { visibility: visible !important; }
+  /* 每页一屏,页间强制分页;组件块尽量不跨页断开 */
+  .print-page { page-break-after: always; }
+  .print-page:last-child { page-break-after: auto; }
+  .print-block { page-break-inside: avoid; }
 }
 </style>

+ 14 - 10
src/views/offerLetterTemplate/mock.js

@@ -26,6 +26,10 @@ export const offerVariables = [
   { code: "reportAddress", label: "报到地址", group: "入职", value: "常德市经开区德山大道88号", sample: "常德市经开区德山大道88号" },
   { code: "contactPerson", label: "联系人", group: "入职", value: "周颖", sample: "周颖" },
   { code: "contactPhone", label: "联系电话", group: "入职", value: "138****6688", sample: "138****6688" },
+  { code: "reportTo", label: "汇报对象", group: "岗位", value: "生产总监", sample: "生产总监" },
+  { code: "companyFullName", label: "公司全称", group: "公司", value: "中英科技有限公司", sample: "中英科技有限公司" },
+  { code: "hrContact", label: "HR 对接人", group: "入职", value: "周颖", sample: "周颖" },
+  { code: "notifyEmail", label: "通知邮箱", group: "入职", value: "hr@example.com", sample: "hr@example.com" },
 ];
 
 // 组件库分组(三栏编辑器左侧),拖拽至画布生成块
@@ -123,16 +127,16 @@ export function createDefaultBlocks() {
   };
   return [
     mk("heading", "录用通知书", { props: { align: "center", size: "h1", style: "box", label: "标题" } }),
-    mk("paragraph", "尊敬的【候选人姓名】:", { props: { align: "left" } }),
-    mk("paragraph", "恭喜您通过我司面试,经研究决定拟录用您担任【录用岗位】,现将录用有关事项通知如下:"),
-    mk("paragraph", "一、录用安排:录用岗位【录用岗位】,所属部门【所属部门】,工作地点【工作地点】。"),
-    mk("paragraph", "二、报到安排:请于【报到日期】前携带相关资料至【报到地址】报到,联系人【联系人】,联系电话【联系电话】。"),
-    mk("paragraph", "三、薪酬待遇:基本工资【基本工资】元/月,岗位工资【岗位工资】元/月,绩效工资【绩效工资】元/月,试用期【试用期】。"),
-    mk("bullet", "四、入职所需材料:\n1. 身份证原件及复印件\n2. 学历学位证书复印件\n3. 离职证明\n4. 一寸免冠照片2张"),
-    mk("paragraph", "五、录用确认:请于【确认截止日期】前确认本通知,逾期未确认视为放弃录用资格。"),
-    mk("paragraph", "六、联系方式:联系人【联系人】,联系电话【联系电话】,如有疑问请及时与我们联系。"),
-    mk("signature", "落款单位:【公司名称】"),
-    mk("date", "签发日期:【签发日期】", { props: { align: "right" } }),
+    mk("paragraph", "致:【候选人姓名】先生/女士:", { props: { align: "left" } }),
+    mk("paragraph", "主题:录用通知 - 【公司名称】 - 【录用岗位】"),
+    mk("paragraph", "您好,恭喜您通过我司面试,我们诚挚邀请您加入【公司名称】。"),
+    mk("paragraph", "一、录用岗位与报到信息:录用岗位【录用岗位】,所属部门【所属部门】,汇报对象【汇报对象】,工作地点【工作地点】,报到时间【报到日期】。"),
+    mk("paragraph", "二、薪酬福利待遇:基本工资【基本工资】元/月,岗位工资【岗位工资】元/月,绩效工资【绩效工资】元/月,试用期条款按公司制度执行。"),
+    mk("bullet", "三、入职所需材料:\n1. 身份证复印件\n2. 学历学位证书复印件\n3. 离职证明\n4. 银行卡信息"),
+    mk("paragraph", "四、录用确认与失效条款:请于【确认截止日期】前确认回复,逾期本通知自动失效。"),
+    mk("paragraph", "五、联系方式:对接人【HR 对接人】,联系电话【联系电话】,邮箱【通知邮箱】。"),
+    mk("signature", "落款单位:【公司全称】人力资源部"),
+    mk("date", "通知签发日期:【签发日期】", { props: { align: "right" } }),
   ];
 }
 

+ 475 - 0
src/views/onboardingContractTemplate/components/ContractTemplateEditor.vue

@@ -0,0 +1,475 @@
+<template>
+  <el-drawer
+    :visible="visible"
+    size="100%"
+    custom-class="contract-editor-drawer"
+    append-to-body
+    :with-header="false"
+    :show-close="false"
+    @update:visible="(v) => $emit('update:visible', v)"
+    @closed="handleClosed"
+  >
+    <div class="contract-editor" :class="{ 'is-fullscreen': fullscreen }">
+      <div class="editor-topbar">
+        <div class="editor-title">
+          <span><i class="el-icon-document"></i></span>
+          <div>
+            <h2>{{ isNew ? "新增合同模板" : "编辑合同模板" }}</h2>
+            <p>左侧组件库拖入画布,选中画布组件在右侧配置属性</p>
+          </div>
+        </div>
+        <div class="editor-top-actions">
+          <el-button size="small" icon="el-icon-refresh" @click="refreshCanvas">刷新画布</el-button>
+          <el-button size="small" :icon="fullscreen ? 'el-icon-aim' : 'el-icon-full-screen'" @click="toggleFullscreen">{{ fullscreen ? "退出全屏" : "全屏编辑" }}</el-button>
+        </div>
+      </div>
+
+      <div class="editor-main">
+        <!-- 左:组件库 -->
+        <aside class="editor-sidebar">
+          <div class="sidebar-head">
+            <h3>模板组件库</h3>
+            <el-button size="mini" icon="el-icon-switch-button" @click="collapsed = !collapsed">{{ collapsed ? "展开" : "收起" }}</el-button>
+          </div>
+          <div v-for="group in componentGroups" :key="group.group" class="component-group" :class="{ collapsed }">
+            <div class="group-name">{{ group.group }}</div>
+            <div v-show="!collapsed" class="component-items">
+              <div
+                v-for="item in group.items"
+                :key="item.type + item.label"
+                class="component-item"
+                draggable="true"
+                @dragstart="dragComponent($event, item)"
+                @click="addBlock(item)"
+              >
+                <i :class="item.icon"></i>
+                <div>
+                  <strong>{{ item.label }}</strong>
+                  <small>{{ item.desc || "自动读取对应档案数据,生成合同时回填" }}</small>
+                </div>
+              </div>
+            </div>
+          </div>
+          <div class="sidebar-tip">
+            <i class="el-icon-info"></i>
+            拖动组件到画布,或点击组件直接添加到末尾。
+          </div>
+        </aside>
+
+        <!-- 中:A4 画布(分页) -->
+        <section class="editor-canvas-wrap">
+          <div class="canvas-ruler">
+            A4 画布 · 所见即所得 · {{变量}} 为动态占位符 · 虚线为自动分页(共 {{ pageCount }} 页)
+          </div>
+          <div ref="canvas" class="offer-canvas contract-canvas" @dragover.prevent @drop="dropComponent($event)">
+            <template v-for="(block, index) in blocks">
+              <div
+                :key="block.id"
+                class="canvas-block"
+                :class="['type-' + block.type, { 'is-selected': selectedIndex === index }]"
+                draggable="true"
+                @click="selectBlock(index)"
+                @dragstart="dragBlock($event, index)"
+                @dragover.prevent
+                @drop="dropBlock($event, index)"
+              >
+                <div class="block-render" :style="blockStyle(block)">
+                  <component
+                    v-if="block.type === 'h1'"
+                    :is="block.props.size || 'h2'"
+                    class="render-heading"
+                    :class="renderClass(block)"
+                  >{{ block.content }}</component>
+                  <div v-else-if="block.type === 'bullet'">
+                    <p v-if="bulletLines(block)[0]" class="render-p" :class="renderClass(block)">{{ bulletLines(block)[0] }}</p>
+                    <ul v-if="bulletLines(block).length > 1" class="render-ul" :class="renderClass(block)">
+                      <li v-for="(line, li) in bulletLines(block).slice(1)" :key="li">{{ line }}</li>
+                    </ul>
+                  </div>
+                  <hr v-else-if="block.type === 'divider'" class="render-divider" />
+                  <div v-else-if="block.type === 'signature'" class="render-signature" :class="renderClass(block)">
+                    <p class="render-p">{{ block.content }}</p>
+                    <div class="signature-area">
+                      <div class="signature-box"><strong>甲方(盖章)</strong><span>签字:____________</span><span>日期:____年__月__日</span></div>
+                      <div class="signature-box"><strong>乙方(签字)</strong><span>签字:____________</span><span>日期:____年__月__日</span></div>
+                    </div>
+                  </div>
+                  <p v-else class="render-p" :class="renderClass(block)">{{ block.content }}</p>
+                </div>
+                <div class="block-ops">
+                  <i class="el-icon-top" @click.stop="moveBlock(index, -1)"></i>
+                  <i class="el-icon-bottom" @click.stop="moveBlock(index, 1)"></i>
+                  <i class="el-icon-delete" @click.stop="removeBlock(index)"></i>
+                </div>
+              </div>
+              <div v-if="pageBreaks.has(index)" :key="`break-${block.id}`" class="page-break-marker">
+                <i></i><span>第 {{ pageIndexOf(index) }} 页</span><i></i>
+              </div>
+            </template>
+            <div v-if="!blocks.length" class="canvas-empty">
+              <i class="el-icon-s-claim"></i>
+              <p>画布为空,从左侧拖入组件开始设计</p>
+            </div>
+          </div>
+        </section>
+
+        <!-- 右:属性面板 -->
+        <aside class="editor-props">
+          <template v-if="selectedBlock">
+            <div class="props-head">
+              <h3>字段属性</h3>
+              <small>{{ componentLabel(selectedBlock.type) }}</small>
+            </div>
+            <div class="props-body">
+              <div class="props-item">
+                <label>标签文字</label>
+                <el-input v-model.trim="selectedBlock.props.label" size="small" placeholder="组件显示名称" />
+              </div>
+              <div class="props-item props-row">
+                <label>是否显示</label>
+                <el-switch v-model="selectedBlock.props.show" />
+              </div>
+              <div class="props-item props-row">
+                <label>是否必填</label>
+                <el-switch v-model="selectedBlock.props.required" />
+              </div>
+              <div class="props-item">
+                <label>数据来源</label>
+                <el-select v-model="selectedBlock.props.source" size="small">
+                  <el-option v-for="o in sourceOptions" :key="o.value" :label="o.label" :value="o.value" />
+                </el-select>
+              </div>
+              <div class="props-item">
+                <label>对齐方式</label>
+                <el-radio-group v-model="selectedBlock.props.align" size="small">
+                  <el-radio-button label="left">左</el-radio-button>
+                  <el-radio-button label="center">中</el-radio-button>
+                  <el-radio-button label="right">右</el-radio-button>
+                </el-radio-group>
+              </div>
+              <div class="props-item">
+                <label>字号</label>
+                <el-input-number v-model="selectedBlock.props.fontSize" :min="10" :max="40" size="small" controls-position="right" class="full-width" />
+              </div>
+              <div class="props-item">
+                <label>块高度</label>
+                <el-select v-model="selectedBlock.props.blockHeight" size="small" allow-create filterable default-first-option>
+                  <el-option label="自适应" value="auto" />
+                  <el-option label="100px" value="100" />
+                  <el-option label="200px" value="200" />
+                  <el-option label="300px" value="300" />
+                </el-select>
+              </div>
+              <div class="props-item">
+                <label>打印字号</label>
+                <el-input-number v-model="selectedBlock.props.printFontSize" :min="8" :max="32" size="small" controls-position="right" class="full-width" />
+              </div>
+              <div class="props-item">
+                <label>章节配置</label>
+                <el-input v-model.trim="selectedBlock.props.sectionConfig" type="textarea" :rows="2" size="small" placeholder="是否打印分割线、补充处理规则等" />
+              </div>
+              <div class="props-actions">
+                <el-button size="small" icon="el-icon-copy-document" @click="copyBlock(selectedIndex)">复制组件</el-button>
+                <el-button size="small" type="danger" plain icon="el-icon-delete" @click="removeBlock(selectedIndex)">删除组件</el-button>
+              </div>
+            </div>
+          </template>
+          <div v-else class="props-empty">
+            <i class="el-icon-click"></i>
+            <p>选中画布中的组件后,在此编辑属性</p>
+          </div>
+        </aside>
+      </div>
+
+      <div class="editor-footer">
+        <span><i class="el-icon-lock"></i> 保存草稿后模板状态为「草稿」,发布后可在「新增合同」页面选用</span>
+        <div>
+          <el-button @click="$emit('update:visible', false)">关闭</el-button>
+          <el-button icon="el-icon-refresh-left" @click="resetCanvas">重置画布</el-button>
+          <el-button icon="el-icon-view" @click="openPreview">打印预览</el-button>
+          <el-button icon="el-icon-document" @click="saveTemplate('DRAFT')">保存草稿</el-button>
+          <el-button type="primary" icon="el-icon-position" @click="saveTemplate('PUBLISHED')">发布模板</el-button>
+        </div>
+      </div>
+
+      <!-- 打印专用容器:仅在打印时显示,按分页逐页输出 -->
+      <div class="print-offer">
+        <div v-for="(page, pIndex) in paginate()" :key="`print-page-${pIndex}`" class="print-page">
+          <div v-for="(blockIndex) in page" :key="blocks[blockIndex].id" class="print-block">
+            <component
+              v-if="blocks[blockIndex].type === 'h1'"
+              :is="blocks[blockIndex].props.size || 'h2'"
+              class="render-heading"
+              :class="renderClass(blocks[blockIndex])"
+            >{{ resolveText(blocks[blockIndex]) }}</component>
+            <div v-else-if="blocks[blockIndex].type === 'bullet'">
+              <p v-if="bulletLines(blocks[blockIndex])[0]" class="render-p" :class="renderClass(blocks[blockIndex])">{{ resolveText({ ...blocks[blockIndex], content: bulletLines(blocks[blockIndex])[0] }) }}</p>
+              <ul v-if="bulletLines(blocks[blockIndex]).length > 1" class="render-ul" :class="renderClass(blocks[blockIndex])">
+                <li v-for="(line, li) in bulletLines(blocks[blockIndex]).slice(1)" :key="li">{{ resolveText({ ...blocks[blockIndex], content: line }) }}</li>
+              </ul>
+            </div>
+            <hr v-else-if="blocks[blockIndex].type === 'divider'" class="render-divider" />
+            <div v-else-if="blocks[blockIndex].type === 'signature'" class="render-signature" :class="renderClass(blocks[blockIndex])">
+              <p class="render-p">{{ resolveText(blocks[blockIndex]) }}</p>
+              <div class="signature-area">
+                <div class="signature-box"><strong>甲方(盖章)</strong><span>签字:____________</span><span>日期:____年__月__日</span></div>
+                <div class="signature-box"><strong>乙方(签字)</strong><span>签字:____________</span><span>日期:____年__月__日</span></div>
+              </div>
+            </div>
+            <p v-else class="render-p" :class="renderClass(blocks[blockIndex])">{{ resolveText(blocks[blockIndex]) }}</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  </el-drawer>
+</template>
+
+<script>
+import {
+  contractVariables,
+  contractComponentGroups,
+  alignOptions,
+  headingSizeOptions,
+  sourceOptions,
+  createContractBlock,
+  createDefaultContractBlocks,
+  resolveContractTokens,
+  contractSampleData,
+} from "../mock";
+
+export default {
+  name: "ContractTemplateEditor",
+  props: {
+    visible: { type: Boolean, default: false },
+    template: { type: Object, default: () => null },
+  },
+  data() {
+    return {
+      collapsed: false,
+      fullscreen: false,
+      blocks: [],
+      selectedIndex: -1,
+      blockHeights: {},
+      componentGroups: contractComponentGroups,
+      contractVariables,
+      alignOptions,
+      headingSizeOptions,
+      sourceOptions,
+      previewData: contractSampleData(),
+      previewing: false,
+      isNew: true,
+    };
+  },
+  computed: {
+    selectedBlock() {
+      return this.blocks[this.selectedIndex] ?? null;
+    },
+    pageBreaks() {
+      const breaks = new Set();
+      const pages = this.paginate();
+      for (let p = 0; p < pages.length - 1; p++) {
+        breaks.add(pages[p][pages[p].length - 1]);
+      }
+      return breaks;
+    },
+    pageCount() {
+      return this.paginate().length;
+    },
+  },
+  watch: {
+    visible(val) {
+      if (val) this.initEditor();
+    },
+    blocks: {
+      deep: true,
+      handler() {
+        this.measureBlocks();
+      },
+    },
+  },
+  methods: {
+    initEditor() {
+      this.isNew = !(this.template && this.template.id);
+      this.blocks = JSON.parse(
+        JSON.stringify(this.template?.blocks?.length ? this.template.blocks : createDefaultContractBlocks()),
+      );
+      this.selectedIndex = -1;
+      this.measureBlocks();
+    },
+    handleClosed() {
+      this.previewing = false;
+      this.selectedIndex = -1;
+    },
+    // ===== 分页 =====
+    measureBlocks() {
+      this.$nextTick(() => {
+        const el = this.$refs.canvas;
+        if (!el) return;
+        const nodes = el.querySelectorAll(".canvas-block");
+        const heights = {};
+        nodes.forEach((node, index) => {
+          const id = this.blocks[index]?.id;
+          if (id) heights[id] = Math.ceil(node.offsetHeight || 0);
+        });
+        this.blockHeights = heights;
+      });
+    },
+    paginate() {
+      const PAGE_HEIGHT = 980;
+      const GAP = 16;
+      const pages = [];
+      let current = [];
+      let used = 24;
+      this.blocks.forEach((block, index) => {
+        const height = this.blockHeights[block.id] || 60;
+        if (current.length && used + height + GAP > PAGE_HEIGHT) {
+          pages.push(current.slice());
+          current = [index];
+          used = height;
+        } else {
+          current.push(index);
+          used += height + GAP;
+        }
+      });
+      if (current.length) pages.push(current);
+      return pages;
+    },
+    pageIndexOf(index) {
+      const pages = this.paginate();
+      for (let p = 0; p < pages.length; p++) {
+        if (pages[p].includes(index)) return p + 1;
+      }
+      return 1;
+    },
+    // ===== 组件库拖拽 =====
+    dragComponent(event, item) {
+      event.dataTransfer.setData("text/plain", JSON.stringify({ type: item.type, variable: item.defaultVariable || "" }));
+    },
+    dropComponent(event) {
+      const payload = this.readDrag(event);
+      if (payload && payload.type) this.blocks.push(createContractBlock(payload.type, payload.variable));
+    },
+    addBlock(item) {
+      this.blocks.push(createContractBlock(item.type, item.defaultVariable || ""));
+    },
+    readDrag(event) {
+      try {
+        const raw = event.dataTransfer.getData("text/plain");
+        return raw ? JSON.parse(raw) : null;
+      } catch (error) {
+        return null;
+      }
+    },
+    dragBlock(event, index) {
+      event.dataTransfer.setData("text/plain", String(index));
+    },
+    dropBlock(event, index) {
+      const from = Number(event.dataTransfer.getData("text/plain"));
+      if (Number.isInteger(from) && from !== index) {
+        const item = this.blocks.splice(from, 1)[0];
+        this.blocks.splice(index, 0, item);
+        this.selectedIndex = index;
+      }
+    },
+    selectBlock(index) {
+      this.selectedIndex = index;
+    },
+    moveBlock(index, delta) {
+      const target = index + delta;
+      if (target < 0 || target >= this.blocks.length) return;
+      const item = this.blocks.splice(index, 1)[0];
+      this.blocks.splice(target, 0, item);
+      this.selectedIndex = target;
+    },
+    removeBlock(index) {
+      this.blocks.splice(index, 1);
+      if (this.selectedIndex === index) this.selectedIndex = -1;
+      else if (this.selectedIndex > index) this.selectedIndex -= 1;
+    },
+    copyBlock(index) {
+      const source = this.blocks[index];
+      if (!source) return;
+      const copy = JSON.parse(JSON.stringify(source));
+      copy.id = `block_${Date.now()}`;
+      copy.content = source.content;
+      this.blocks.splice(index + 1, 0, copy);
+      this.selectedIndex = index + 1;
+    },
+    componentLabel(type) {
+      return contractComponentGroups.flatMap((g) => g.items).find((i) => i.type === type)?.label || type;
+    },
+    bulletLines(block) {
+      return String(block.content || "").split("\n").map((l) => l.trim()).filter(Boolean);
+    },
+    renderClass(block) {
+      const p = block.props || {};
+      return {
+        "align-left": p.align === "left",
+        "align-center": p.align === "center",
+        "align-right": p.align === "right",
+      };
+    },
+    blockStyle(block) {
+      const p = block.props || {};
+      return {
+        fontSize: `${p.fontSize || 16}px`,
+        minHeight: p.blockHeight && p.blockHeight !== "auto" ? `${p.blockHeight}px` : "auto",
+      };
+    },
+    resolveText(block) {
+      return resolveContractTokens(block.content, this.previewing ? this.previewData : {});
+    },
+    // ===== 右上角 =====
+    refreshCanvas() {
+      this.blocks = JSON.parse(
+        JSON.stringify(this.template?.blocks?.length ? this.template.blocks : createDefaultContractBlocks()),
+      );
+      this.$message.success("画布已恢复至上一次保存状态");
+    },
+    toggleFullscreen() {
+      this.fullscreen = !this.fullscreen;
+    },
+    // ===== 底部操作 =====
+    resetCanvas() {
+      this.$confirm("重置将清空画布全部内容,恢复空白模板,确定吗?", "重置画布", { type: "warning" })
+        .then(() => {
+          this.blocks = [];
+          this.selectedIndex = -1;
+          this.$message.success("画布已重置");
+        })
+        .catch(() => {});
+    },
+    openPreview() {
+      if (!this.blocks.length) return this.$message.warning("画布内容为空,请先添加组件");
+      this.previewing = true;
+      this.$nextTick(() => window.print());
+    },
+    saveTemplate(status) {
+      if (!this.blocks.length) return this.$message.warning("画布内容为空,请先添加组件");
+      this.$emit("save", {
+        ...(this.template || {}),
+        status,
+        blocks: JSON.parse(JSON.stringify(this.blocks)),
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped src="../../../styles/views/onboardingContractTemplate/editor.scss"></style>
+<style lang="scss">
+/* 打印合同:直接进入浏览器打印,按分页逐页输出 */
+.print-offer { display: none; }
+@media print {
+  @page { size: A4; margin: 14mm 12mm; }
+  body * { visibility: hidden !important; }
+  .contract-editor-drawer .editor-topbar,
+  .contract-editor-drawer .editor-main,
+  .contract-editor-drawer .editor-footer { display: none !important; }
+  .print-offer { display: block !important; width: 100%; visibility: visible !important; }
+  .print-offer * { visibility: visible !important; }
+  .print-page { page-break-after: always; }
+  .print-page:last-child { page-break-after: auto; }
+  .print-block { page-break-inside: avoid; }
+}
+</style>

+ 86 - 12
src/views/onboardingContractTemplate/index.vue

@@ -274,6 +274,13 @@
         >
       </div>
     </el-drawer>
+
+    <!-- 三栏模板编辑器(新增/编辑) -->
+    <contract-template-editor
+      :visible.sync="editorVisible"
+      :template="editingTemplate"
+      @save="handleContractEditorSave"
+    />
   </main>
 </template>
 <script>
@@ -287,6 +294,8 @@ import {
   enableContractTemplate,
 } from "@/api/hr";
 import { listOrganizations } from "@/api/organization";
+import ContractTemplateEditor from "./components/ContractTemplateEditor.vue";
+import { contractTemplateSeeds } from "./mock";
 // 合同类型枚举
 const CONTRACT_TYPES = [
   { value: "FIXED", label: "固定期限" },
@@ -393,11 +402,16 @@ const blank = () => ({
 });
 export default {
   name: "OnboardingContractTemplate",
+  components: { ContractTemplateEditor },
   data() {
     return {
       loading: false,
       rows: [],
       totalCount: 0,
+      // 三栏模板编辑器
+      editorVisible: false,
+      editingTemplate: null,
+      editorStoreKey: "oa-contract-editor-templates-v1",
       filters: { keyword: "", type: "", enabled: "" },
       contractTypes: CONTRACT_TYPES,
       sourceTypes: SOURCE_TYPES,
@@ -509,9 +523,31 @@ export default {
           : "DISABLED",
       });
       const list = (data?.list || []).map(adaptTemplate);
-      this.rows = list;
-      this.totalCount = Number(data?.count ?? 0);
-      return { list, count: this.totalCount };
+      // 合并三栏编辑器维护的模板(仅第一页前置展示)
+      const editorList =
+        page === 1
+          ? this.loadEditorStore().map((item) => ({
+              id: item.id,
+              code: item.code || "",
+              name: item.name || "",
+              type: "",
+              typeLabel: "合同模板",
+              legalId: item.legalId,
+              legal: item.legal || "",
+              enabled: item.status === "PUBLISHED",
+              version: item.version || "V1.0",
+              remark: item.remark || "",
+              auditRaw: item.status === "PUBLISHED" ? "APPROVED" : "NOT_SUBMITTED",
+              audit: item.status === "PUBLISHED" ? "已发布" : "草稿",
+              fields: [],
+              blocks: item.blocks || [],
+              updatedAt: item.updatedAt || "",
+            }))
+          : [];
+      const merged = [...editorList, ...list];
+      this.rows = merged;
+      this.totalCount = Number(data?.count ?? 0) + editorList.length;
+      return { list: merged, count: this.totalCount };
     },
     reload() {
       this.$refs.table?.reload();
@@ -530,10 +566,53 @@ export default {
       await this.loadDetail(row.id);
     },
     async edit(row) {
-      this.mode = "edit";
-      this.drawer = true;
-      this.form = this.clone(row);
-      await this.loadDetail(row.id);
+      this.openContractEditor(row);
+    },
+    // 打开三栏模板编辑器(新增/编辑)
+    openContractEditor(row) {
+      this.editingTemplate = row ? this.clone(row) : null;
+      this.editorVisible = true;
+    },
+    openCreate() {
+      this.openContractEditor(null);
+    },
+    // 编辑器保存:写入 localStorage 并合并到列表(草稿/发布)
+    handleContractEditorSave(template) {
+      const now = new Date().toISOString().slice(0, 16).replace("T", " ");
+      const store = this.loadEditorStore();
+      let record;
+      if (template.id) {
+        const index = store.findIndex((item) => item.id === template.id);
+        if (index > -1) {
+          record = { ...store[index], ...template, updatedAt: now };
+          store[index] = record;
+        }
+      } else {
+        record = {
+          id: Date.now(),
+          code: `HT-EDIT-${String(store.length + 1).padStart(3, "0")}`,
+          name: "未命名合同模板",
+          legalId: this.legalEntities[0]?.id ?? null,
+          legal: this.legalEntities[0]?.name || "",
+          status: template.status === "PUBLISHED" ? "PUBLISHED" : "DRAFT",
+          version: "V1.0",
+          remark: "",
+          blocks: template.blocks,
+          updatedAt: now,
+        };
+        store.unshift(record);
+      }
+      localStorage.setItem(this.editorStoreKey, JSON.stringify(store));
+      this.$message.success(template.status === "PUBLISHED" ? "合同模板已发布" : "合同模板草稿已保存");
+      this.editorVisible = false;
+      this.reload();
+    },
+    loadEditorStore() {
+      try {
+        return JSON.parse(localStorage.getItem(this.editorStoreKey) || "null") || contractTemplateSeeds;
+      } catch (error) {
+        return contractTemplateSeeds;
+      }
     },
     async loadDetail(id) {
       if (!id) return;
@@ -544,11 +623,6 @@ export default {
         this.$message.error(error.message || "模板详情加载失败");
       }
     },
-    openCreate() {
-      this.mode = "create";
-      this.form = blank();
-      this.drawer = true;
-    },
     closeDrawer(done) {
       if (this.mode === "view") {
         this.drawer = false;

+ 203 - 0
src/views/onboardingContractTemplate/mock.js

@@ -0,0 +1,203 @@
+// 入职合同模板管理系统 - 离线 mock 数据(后续接入接口后替换为 API 数据源)
+
+// 法人兜底
+export const fallbackLegals = [
+  { id: 1, name: "中英科技有限公司" },
+  { id: 2, name: "长沙中盈制造有限公司" },
+  { id: 3, name: "常德精工机械有限公司" },
+];
+
+// 合同系统变量库:法人固定字段 + 员工动态变量,生成合同时按 label 替换为真实数据
+export const contractVariables = [
+  // 法人固定字段
+  { code: "companyName", label: "公司名称", group: "法人固定字段", source: "legal", value: "中英科技有限公司", sample: "中英科技有限公司" },
+  { code: "companyAddress", label: "公司注册地址", group: "法人固定字段", source: "legal", value: "长沙市岳麓区XX路88号", sample: "长沙市岳麓区XX路88号" },
+  { code: "legalRepresentative", label: "法定代表人", group: "法人固定字段", source: "legal", value: "张伟", sample: "张伟" },
+  // 员工动态变量
+  { code: "employeeName", label: "员工姓名", group: "员工动态变量", source: "employee", value: "刘启航", sample: "刘启航" },
+  { code: "gender", label: "性别", group: "员工动态变量", source: "employee", value: "男", sample: "男" },
+  { code: "idCard", label: "身份证号", group: "员工动态变量", source: "employee", value: "430121199505120038", sample: "430121199505120038" },
+  { code: "address", label: "通讯地址", group: "员工动态变量", source: "employee", value: "常德市经开区德山大道88号", sample: "常德市经开区德山大道88号" },
+  { code: "position", label: "岗位", group: "员工动态变量", source: "employee", value: "数控车床操作工", sample: "数控车床操作工" },
+  { code: "contractStartDate", label: "合同起始日期", group: "员工动态变量", source: "employee", value: "2026-09-10", sample: "2026-09-10" },
+  { code: "contractEndDate", label: "合同终止日期", group: "员工动态变量", source: "employee", value: "2029-09-09", sample: "2029-09-09" },
+  { code: "monthlySalary", label: "月薪资", group: "员工动态变量", source: "employee", value: "8000", sample: "8000" },
+  { code: "probationSalary", label: "试用期薪资", group: "员工动态变量", source: "employee", value: "6400", sample: "6400" },
+  { code: "workLocation", label: "工作地点", group: "员工动态变量", source: "employee", value: "常德工厂", sample: "常德工厂" },
+];
+
+// 组件库分组(三栏编辑器左侧)
+export const contractComponentGroups = [
+  {
+    group: "法人固定字段",
+    items: [
+      { type: "variable", label: "公司名称", icon: "el-icon-office-building", defaultVariable: "companyName" },
+      { type: "variable", label: "公司注册地址", icon: "el-icon-location-outline", defaultVariable: "companyAddress" },
+      { type: "variable", label: "法定代表人", icon: "el-icon-user", defaultVariable: "legalRepresentative" },
+    ],
+  },
+  {
+    group: "员工动态变量",
+    items: [
+      { type: "variable", label: "员工姓名", icon: "el-icon-user-solid", defaultVariable: "employeeName" },
+      { type: "variable", label: "性别", icon: "el-icon-male", defaultVariable: "gender" },
+      { type: "variable", label: "身份证号", icon: "el-icon-postcard", defaultVariable: "idCard" },
+      { type: "variable", label: "通讯地址", icon: "el-icon-location", defaultVariable: "address" },
+      { type: "variable", label: "岗位", icon: "el-icon-suitcase", defaultVariable: "position" },
+      { type: "variable", label: "合同起始日期", icon: "el-icon-date", defaultVariable: "contractStartDate" },
+      { type: "variable", label: "合同终止日期", icon: "el-icon-date", defaultVariable: "contractEndDate" },
+      { type: "variable", label: "月薪资", icon: "el-icon-coin", defaultVariable: "monthlySalary" },
+      { type: "variable", label: "试用期薪资", icon: "el-icon-coin", defaultVariable: "probationSalary" },
+      { type: "variable", label: "工作地点", icon: "el-icon-location-outline", defaultVariable: "workLocation" },
+    ],
+  },
+  {
+    group: "通用排版组件",
+    items: [
+      { type: "h1", label: "一级标题", icon: "el-icon-postcard", desc: "文档大标题、章节标题" },
+      { type: "paragraph", label: "富文本段落", icon: "el-icon-tickets", desc: "大段法律条文、合同正文内容编辑" },
+      { type: "contractTerm", label: "合同期限组件", icon: "el-icon-time", desc: "固定期限 / 无固定期限 / 以完成一定工作任务为期限" },
+      { type: "radio", label: "单选组件", icon: "el-icon-radio", desc: "合同类型、选项条款单选(如是否试用、是否竞业等)" },
+      { type: "divider", label: "分割横线", icon: "el-icon-minus", desc: "章节分割线" },
+      { type: "signature", label: "签名栏组件", icon: "el-icon-edit-outline", desc: "甲方盖章栏、乙方员工签字栏,含签字及日期占位" },
+    ],
+  },
+];
+
+// 文本样式 / 对齐 / 标题级别 枚举
+export const alignOptions = [
+  { value: "left", label: "左对齐" },
+  { value: "center", label: "居中" },
+  { value: "right", label: "右对齐" },
+];
+export const headingSizeOptions = [
+  { value: "h1", label: "一级标题" },
+  { value: "h2", label: "二级标题" },
+  { value: "h3", label: "三级标题" },
+];
+export const sourceOptions = [
+  { value: "legal", label: "法人档案自动回填" },
+  { value: "employee", label: "员工档案自动回填" },
+  { value: "static", label: "静态固定文字" },
+];
+
+const defaultProps = () => ({
+  label: "",
+  variable: "",
+  show: true,
+  required: false,
+  source: "",
+  align: "left",
+  fontSize: 16,
+  blockHeight: "auto",
+  printFontSize: 12,
+  sectionConfig: "",
+  size: "h2",
+});
+
+let blockSeq = 0;
+// 依据组件类型创建默认块
+export function createContractBlock(type, variableCode = "") {
+  const props = defaultProps();
+  const varDef = contractVariables.find((v) => v.code === variableCode) || null;
+  if (varDef) {
+    props.label = varDef.label;
+    props.variable = varDef.code;
+    props.source = varDef.source;
+  }
+  const token = varDef ? `{{${varDef.label}}}` : "";
+  const block = { id: `block_${++blockSeq}`, type, props, content: "" };
+  if (type === "variable") {
+    block.content = token;
+  } else if (type === "h1") {
+    block.content = "新章节标题";
+    props.align = "center";
+    props.size = "h1";
+  } else if (type === "paragraph") {
+    block.content = "请输入合同条款内容,可插入{{员工姓名}}等动态变量";
+  } else if (type === "contractTerm") {
+    block.content = "本合同期限为:□ 固定期限 □ 无固定期限 □ 以完成一定工作任务为期限";
+  } else if (type === "radio") {
+    block.content = "□ 是 □ 否";
+  } else if (type === "divider") {
+    block.content = "";
+  } else if (type === "signature") {
+    block.content = "";
+    props.align = "left";
+  }
+  return block;
+}
+
+// 劳动合同默认骨架(对应业务文档章节)
+export function createDefaultContractBlocks() {
+  const mk = (type, content, extra = {}) => {
+    const block = createContractBlock(type);
+    Object.assign(block, { content }, extra.props ? { props: { ...block.props, ...extra.props } } : {});
+    return block;
+  };
+  return [
+    mk("h1", "劳动合同", { props: { align: "center", label: "合同标题" } }),
+    mk("paragraph", "甲方(用人单位):{{公司名称}}\n统一社会信用代码:________________\n住所:{{公司注册地址}}\n法定代表人:{{法定代表人}}"),
+    mk("paragraph", "乙方(员工):{{员工姓名}}\n性别:{{性别}} 身份证号:{{身份证号}}\n通讯地址:{{通讯地址}}\n联系电话:________________"),
+    mk("paragraph", "根据《中华人民共和国劳动法》《中华人民共和国劳动合同法》及相关法律法规,甲乙双方本着平等自愿、协商一致的原则,订立本合同,共同遵守。"),
+    mk("paragraph", "一、劳动合同期限:本合同自{{合同起始日期}}起至{{合同终止日期}}止。", { props: { label: "劳动合同期限" } }),
+    mk("paragraph", "二、工作内容与工作地点:乙方担任{{岗位}},工作地点{{工作地点}}。"),
+    mk("paragraph", "三、工作时间与休息休假:执行国家规定的标准工时制度,依法享受休息休假权利。"),
+    mk("paragraph", "四、薪资待遇:乙方月薪资为{{月薪资}}元/月,试用期薪资为{{试用期薪资}}元/月。"),
+    mk("paragraph", "五、保险福利:甲方依法为乙方缴纳社会保险及住房公积金。"),
+    mk("paragraph", "六、劳动纪律:乙方应遵守甲方依法制定的规章制度及劳动纪律。"),
+    mk("paragraph", "七、合同变更、解除与终止:按国家法律法规及双方约定执行。"),
+    mk("paragraph", "八、违约责任:任何一方违反本合同约定,应承担相应法律责任。"),
+    mk("paragraph", "九、双方约定补充条款:________________________________________。"),
+    mk("signature", "甲方(盖章):{{公司名称}}  法定代表人/授权代表:{{法定代表人}}\n乙方(签字):{{员工姓名}}  日期:____年__月__日", { props: { label: "签字盖章" } }),
+  ];
+}
+
+// 替换 {{变量label}} 占位符;无数据时保留占位符
+export function resolveContractTokens(text, dataByLabel) {
+  return String(text || "").replace(/\{\{([^}]+)\}\}/g, (m, label) => {
+    const v = dataByLabel[label];
+    return v !== undefined && v !== null && v !== "" ? v : m;
+  });
+}
+
+// 由 contractVariables 构建「label → 预览值」映射(打印预览用)
+export function contractSampleData() {
+  const map = {};
+  contractVariables.forEach((v) => {
+    map[v.label] = v.sample || v.value;
+  });
+  return map;
+}
+
+// 模板种子数据
+export const contractTemplateSeeds = [
+  {
+    id: 1,
+    code: "HT-202608-001",
+    name: "标准劳动合同",
+    legalId: 1,
+    legal: "中英科技有限公司",
+    status: "PUBLISHED",
+    version: "V1.0",
+    remark: "适用固定期限正式员工,含完整条款。",
+    blocks: createDefaultContractBlocks(),
+    updatedAt: "2026-08-20 10:24",
+  },
+  {
+    id: 2,
+    code: "HT-202607-002",
+    name: "劳务派遣合同模板",
+    legalId: 2,
+    legal: "长沙中盈制造有限公司",
+    status: "DRAFT",
+    version: "V0.9",
+    remark: "适用劳务派遣用工场景。",
+    blocks: createDefaultContractBlocks().slice(0, 10),
+    updatedAt: "2026-08-18 09:12",
+  },
+];
+
+export function clone(value) {
+  return JSON.parse(JSON.stringify(value));
+}

+ 173 - 2
src/views/organization/components/OrganizationEditDialog.vue

@@ -136,6 +136,68 @@
         </el-col>
       </el-row>
     </el-form>
+
+      <!-- 部门信息:非 tab,直接置于组织表单底部,以分区样式区分 -->
+      <template v-if="showDepartment">
+        <div class="section-title">
+          <span><i class="el-icon-s-grid"></i></span>
+          <div>
+            <strong>部门信息</strong>
+            <small>配置部门岗位、岗位层级与编制人数,保存后同步部门岗位关联</small>
+          </div>
+        </div>
+        <el-form ref="deptForm" :model="deptForm" label-position="top">
+          <el-row :gutter="20">
+            <el-col :xs="24" :sm="12">
+              <el-form-item label="岗位名称" prop="positionId">
+                <el-select
+                  v-model="deptForm.positionId"
+                  filterable
+                  clearable
+                  placeholder="请选择岗位"
+                  class="full-width"
+                  @change="onPositionChange"
+                >
+                  <el-option
+                    v-for="position in positions"
+                    :key="position.id"
+                    :label="position.positionName"
+                    :value="position.id"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :sm="12">
+              <el-form-item label="岗位层级" prop="positionLevel">
+                <el-select
+                  v-model="deptForm.positionLevel"
+                  clearable
+                  placeholder="请选择岗位层级"
+                  class="full-width"
+                >
+                  <el-option
+                    v-for="level in levelOptions"
+                    :key="level"
+                    :label="level"
+                    :value="level"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :sm="12">
+              <el-form-item label="编制人数" prop="establishmentCount">
+                <el-input-number
+                  v-model="deptForm.establishmentCount"
+                  :min="0"
+                  :max="9999"
+                  controls-position="right"
+                  class="full-width"
+                />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
+      </template>
       </el-tab-pane>
 
       <el-tab-pane v-if="showEnterprise" label="企业信息" name="enterprise">
@@ -250,6 +312,7 @@
         </el-row>
       </el-form>
       </el-tab-pane>
+
     </el-tabs>
 
     <template v-slot:footer>
@@ -277,8 +340,11 @@ import {
   addOrganization,
   getOrganization,
   getOrganizationCode,
+  listOrganizations,
+  updateDepartmentPosition,
   updateOrganization,
 } from "@/api/organization";
+import { getPositionPage } from "@/api/hr";
 import { downloadFile, removeFile, uploadFile } from "@/api/file";
 import OrganizationSelect from './OrganizationSelect.vue';
 import StaffSelection from './StaffSelection.vue';
@@ -295,6 +361,12 @@ const defaultForm = () => ({
   manager: [],
 });
 
+const defaultDeptForm = () => ({
+  positionId: null,
+  positionLevel: "",
+  establishmentCount: 0,
+});
+
 const defaultEnterprise = () => ({
   businessLicenseFile: [],
   name: "",
@@ -326,10 +398,13 @@ export default {
     return {
       form: defaultForm(),
       enterprise: defaultEnterprise(),
+      deptForm: defaultDeptForm(),
       saving: false,
       loadingDetail: false,
       activeTab: "organization",
       parentCheckedKeys: [],
+      // 岗位设定列表(部门信息-岗位名称下拉数据)
+      positions: [],
     };
   },
   computed: {
@@ -339,6 +414,17 @@ export default {
     showEnterprise() {
       return ["10", "20"].includes(String(this.form.type));
     },
+    // 部门类型:显示「部门信息」分区(置于组织表单底部)
+    showDepartment() {
+      return String(this.form.type) === "30";
+    },
+    // 岗位层级:随所选岗位联动(取岗位的 positionLevels)
+    levelOptions() {
+      const position = this.positions.find(
+        (item) => String(item.id) === String(this.deptForm.positionId),
+      );
+      return position?.positionLevels || [];
+    },
     firstManagerName() {
       const user = this.form.managerFirst?.[0];
       if (!user) return '';
@@ -398,7 +484,9 @@ export default {
       this.activeTab = "organization";
       this.form = defaultForm();
       this.enterprise = defaultEnterprise();
+      this.deptForm = defaultDeptForm();
       this.form.parentId = this.parentId || null;
+      this.loadPositions();
       if (this.isUpdate) {
         this.loadingDetail = true;
         try {
@@ -461,6 +549,69 @@ export default {
         this.$refs.enterpriseForm.validate((valid) => resolve(valid));
       });
     },
+    // 加载岗位设定列表(岗位名称下拉数据源)
+    async loadPositions() {
+      try {
+        const data = await getPositionPage({ pageNum: 1, size: 1000, status: 1 });
+        this.positions = (data?.list || []).filter((item) => item.positionName);
+      } catch (error) {
+        this.positions = [];
+      }
+    },
+    // 切换岗位时清空已选层级(联动)
+    onPositionChange() {
+      this.deptForm.positionLevel = "";
+    },
+    positionLabel(positionId) {
+      return this.positions.find((item) => String(item.id) === String(positionId))
+        ?.positionName || "";
+    },
+    validateDepartment() {
+      if (!this.showDepartment) {
+        return Promise.resolve(true);
+      }
+      if (!this.deptForm.positionId) {
+        this.$message.warning("部门类型请选择岗位名称");
+        return Promise.resolve(false);
+      }
+      return Promise.resolve(true);
+    },
+    // 保存部门岗位关联(PUT /fm/tmaindepartmentposition/updateBatch)
+    async saveDepartmentPosition(payload, result) {
+      let deptId = payload.id || this.data?.id;
+      if (!deptId && result && typeof result === "object") {
+        deptId = result.id;
+      }
+      if (!deptId) {
+        // 新增后通过组织编码反查组织 id
+        try {
+          const list = await listOrganizations();
+          const created = (Array.isArray(list) ? list : []).find(
+            (item) => item.groupCode === payload.groupCode,
+          );
+          deptId = created?.id ?? null;
+        } catch (error) {
+          deptId = null;
+        }
+      }
+      if (!deptId) {
+        this.$message.warning("组织已保存,但未能获取组织ID,部门岗位编制未保存");
+        return;
+      }
+      const position = this.positions.find(
+        (item) => String(item.id) === String(this.deptForm.positionId),
+      );
+      await updateDepartmentPosition([
+        {
+          deptId,
+          positionId: this.deptForm.positionId,
+          positionLevel: this.deptForm.positionLevel || position?.positionLevels?.[0] || "",
+          positionName: position?.positionName || "",
+          establishmentCount: Number(this.deptForm.establishmentCount) || 0,
+        },
+      ]);
+      this.$message.success("部门岗位编制已保存");
+    },
     beforeLicenseUpload(file) {
       if (file.size / 1024 / 1024 > 10) {
         this.$message.error("附件大小不能超过 10MB");
@@ -525,6 +676,10 @@ export default {
           this.activeTab = "enterprise";
           return;
         }
+        const departmentValid = await this.validateDepartment();
+        if (!departmentValid) {
+          return;
+        }
         this.saving = true;
         try {
           const payload = {
@@ -537,10 +692,25 @@ export default {
           if (!this.isUpdate) {
             delete payload.id;
           }
-          const message = this.isUpdate
+          const result = this.isUpdate
             ? await updateOrganization(payload)
             : await addOrganization(payload);
-          this.$message.success(message || "保存成功");
+          this.$message.success(
+            typeof result === "string"
+              ? result || "保存成功"
+              : result?.message || "保存成功",
+          );
+          // 部门类型:同步保存部门岗位关联(编制人数)
+          if (this.showDepartment && this.deptForm.positionId) {
+            try {
+              await this.saveDepartmentPosition(payload, result);
+            } catch (error) {
+              this.$message.warning(
+                "组织已保存,但部门岗位编制保存失败:" +
+                  (error.message || "请稍后重试"),
+              );
+            }
+          }
           this.updateVisible(false);
           this.$emit("done");
         } catch (error) {
@@ -553,6 +723,7 @@ export default {
     reset() {
       this.form = defaultForm();
       this.enterprise = defaultEnterprise();
+      this.deptForm = defaultDeptForm();
       this.activeTab = "organization";
       this.parentCheckedKeys = [];
       this.$refs.form?.clearValidate();