Explorar el Código

fix: 个人简历内容调整

zhangying hace 2 días
padre
commit
939ac85ca6

+ 15 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personal/controller/ResumeInfoController.java

@@ -88,6 +88,21 @@ public class ResumeInfoController extends JeecgController<ResumeInfo, IResumeInf
         return Result.OK("添加成功!");
     }
 
+    /**
+     * 快速新增简历(仅保存基本信息:简历名称、有效期、是否公开、是否默认简历)
+     *
+     * @param resumeInfo
+     * @return 返回新增简历的ID
+     */
+    @AutoLog(value = "个人信息-简历信息-快速新增")
+    @Operation(summary = "个人信息-简历信息-快速新增")
+    @RequiresPermissions("personal:resume_info:add")
+    @PostMapping(value = "/addBasic")
+    public Result<String> addBasic(@RequestBody ResumeInfo resumeInfo) {
+        resumeInfoService.save(resumeInfo);
+        return Result.OK(resumeInfo.getId(), "添加成功!");
+    }
+
     /**
      * 编辑
      *

+ 4 - 0
jeecgboot-vue3/src/views/recruitment/personal/ResumeInfo.api.ts

@@ -6,6 +6,7 @@ enum Api {
   save = '/personal/resumeInfo/add',
   edit = '/personal/resumeInfo/edit',
   deleteOne = '/personal/resumeInfo/delete',
+  addBasic = '/personal/resumeInfo/addBasic',
 }
 
 export const resumeList = (params) => defHttp.get({ url: Api.list, params });
@@ -21,3 +22,6 @@ export const deleteResume = (params, handleSuccess) => {
     handleSuccess();
   });
 };
+
+/** 快速新增简历(仅保存基本信息) */
+export const addBasicResume = (params) => defHttp.post({ url: Api.addBasic, params }, { isTransformResponse: false });

+ 105 - 0
jeecgboot-vue3/src/views/recruitment/personal/components/ResumeAwardModal.vue

@@ -0,0 +1,105 @@
+<template>
+  <j-modal :title="formData.id ? '编辑获奖情况' : '新增获奖情况'" :visible="visible" :width="700" centered @ok="handleOk" @cancel="handleCancel" :confirmLoading="loading">
+    <div style="padding: 20px;">
+      <a-form ref="formRef" :model="formData" :labelCol="{ span: 6 }" :wrapperCol="{ span: 16 }">
+        <a-row :gutter="24">
+          <a-col :span="12">
+            <a-form-item label="证书名称" name="certificateName" :rules="[{ required: true, message: '请输入证书名称' }]">
+              <a-input v-model:value="formData.certificateName" allow-clear placeholder="请输入证书名称"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="发证日期" name="issueDate" :rules="[{ required: true, message: '请选择发证日期' }]">
+              <a-date-picker v-model:value="formData.issueDate" allow-clear placeholder="请选择发证日期" style="width: 100%" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="颁奖机构" name="awardOrganization">
+              <a-input v-model:value="formData.awardOrganization" allow-clear placeholder="请输入颁奖机构"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="角色" name="role">
+              <a-input v-model:value="formData.role" allow-clear placeholder="请输入角色"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="成果名称" name="achievementName">
+              <a-input v-model:value="formData.achievementName" allow-clear placeholder="请输入成果名称"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="成果说明" name="achievementDesc">
+              <a-textarea v-model:value="formData.achievementDesc" :rows="2" allow-clear placeholder="请输入成果说明"></a-textarea>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="其他说明" name="otherDesc">
+              <a-textarea v-model:value="formData.otherDesc" :rows="2" allow-clear placeholder="请输入其他说明"></a-textarea>
+            </a-form-item>
+          </a-col>
+        </a-row>
+      </a-form>
+    </div>
+  </j-modal>
+</template>
+
+<script lang="ts" setup>
+  import { defineExpose, reactive, ref } from 'vue';
+  import JModal from '/@/components/Modal/src/JModal/JModal.vue';
+  import dayjs from 'dayjs';
+
+  const emit = defineEmits(['success']);
+  const visible = ref<boolean>(false);
+  const loading = ref<boolean>(false);
+  const formRef = ref();
+  const formData = reactive<Record<string, any>>({
+    id: '',
+    certificateName: '',
+    issueDate: null,
+    awardOrganization: '',
+    role: '',
+    achievementName: '',
+    achievementDesc: '',
+    otherDesc: '',
+  });
+
+  function open(record?: Record<string, any>) {
+    formData.id = '';
+    formData.certificateName = '';
+    formData.issueDate = null;
+    formData.awardOrganization = '';
+    formData.role = '';
+    formData.achievementName = '';
+    formData.achievementDesc = '';
+    formData.otherDesc = '';
+    if (record) {
+      Object.assign(formData, record);
+      if (formData.issueDate && typeof formData.issueDate === 'string') formData.issueDate = dayjs(formData.issueDate);
+    }
+    visible.value = true;
+  }
+
+  async function handleOk() {
+    try {
+      await formRef.value?.validate();
+    } catch {
+      return;
+    }
+    loading.value = true;
+    try {
+      const result = { ...formData };
+      if (result.issueDate && dayjs.isDayjs(result.issueDate)) result.issueDate = result.issueDate.format('YYYY-MM-DD');
+      emit('success', result);
+      visible.value = false;
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  function handleCancel() {
+    visible.value = false;
+  }
+
+  defineExpose({ open });
+</script>

+ 114 - 0
jeecgboot-vue3/src/views/recruitment/personal/components/ResumeEducationModal.vue

@@ -0,0 +1,114 @@
+<template>
+  <j-modal :title="formData.id ? '编辑教育经历' : '新增教育经历'" :visible="visible" :width="700" centered @ok="handleOk" @cancel="handleCancel" :confirmLoading="loading">
+    <div style="padding: 20px;">
+      <a-form ref="formRef" :model="formData" :labelCol="{ span: 6 }" :wrapperCol="{ span: 16 }">
+        <a-row :gutter="24">
+          <a-col :span="12">
+            <a-form-item label="开始时间" name="startDate" :rules="[{ required: true, message: '请选择开始时间' }]">
+              <a-date-picker v-model:value="formData.startDate" allow-clear placeholder="请选择开始时间" style="width: 100%" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="结束时间" name="endDate" :rules="[{ required: true, message: '请选择结束时间' }]">
+              <a-date-picker v-model:value="formData.endDate" allow-clear placeholder="请选择结束时间" style="width: 100%" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="毕业院校" name="schoolName" :rules="[{ required: true, message: '请输入毕业院校' }]">
+              <a-input v-model:value="formData.schoolName" allow-clear placeholder="请输入毕业院校"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="所学专业" name="major">
+              <a-input v-model:value="formData.major" allow-clear placeholder="请输入所学专业"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="学历" name="education" :rules="[{ required: true, message: '请选择学历' }]">
+              <a-select v-model:value="formData.education" :options="educationOptions" allow-clear placeholder="请选择学历" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="学位" name="degree">
+              <a-select v-model:value="formData.degree" :options="degreeOptions" allow-clear placeholder="请选择学位" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="学制" name="eduSystem">
+              <a-select v-model:value="formData.eduSystem" :options="eduSystemOptions" allow-clear placeholder="请选择学制" />
+            </a-form-item>
+          </a-col>
+        </a-row>
+      </a-form>
+    </div>
+  </j-modal>
+</template>
+
+<script lang="ts" setup>
+  import { defineExpose, reactive, ref } from 'vue';
+  import JModal from '/@/components/Modal/src/JModal/JModal.vue';
+  import { useDict } from '/@/hooks/dictionary/useDict';
+  import { computed } from 'vue';
+  import dayjs from 'dayjs';
+
+  const { getDictOptions } = useDict(['Education', 'Degree', 'EduSystem']);
+  const educationOptions = computed(() => getDictOptions('Education'));
+  const degreeOptions = computed(() => getDictOptions('Degree'));
+  const eduSystemOptions = computed(() => getDictOptions('EduSystem'));
+
+  const emit = defineEmits(['success']);
+  const visible = ref<boolean>(false);
+  const loading = ref<boolean>(false);
+  const formRef = ref();
+  const formData = reactive<Record<string, any>>({
+    id: '',
+    startDate: null,
+    endDate: null,
+    schoolName: '',
+    major: '',
+    education: '',
+    degree: '',
+    eduSystem: '',
+  });
+
+  function open(record?: Record<string, any>) {
+    formData.id = '';
+    formData.startDate = null;
+    formData.endDate = null;
+    formData.schoolName = '';
+    formData.major = '';
+    formData.education = '';
+    formData.degree = '';
+    formData.eduSystem = '';
+    if (record) {
+      Object.assign(formData, record);
+      if (formData.startDate && typeof formData.startDate === 'string') formData.startDate = dayjs(formData.startDate);
+      if (formData.endDate && typeof formData.endDate === 'string') formData.endDate = dayjs(formData.endDate);
+    }
+    visible.value = true;
+  }
+
+  async function handleOk() {
+    try {
+      await formRef.value?.validate();
+    } catch {
+      return;
+    }
+    loading.value = true;
+    try {
+      const result = { ...formData };
+      if (result.startDate && dayjs.isDayjs(result.startDate)) result.startDate = result.startDate.format('YYYY-MM-DD');
+      if (result.endDate && dayjs.isDayjs(result.endDate)) result.endDate = result.endDate.format('YYYY-MM-DD');
+      emit('success', result);
+      visible.value = false;
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  function handleCancel() {
+    visible.value = false;
+  }
+
+  defineExpose({ open });
+</script>

+ 22 - 2
jeecgboot-vue3/src/views/recruitment/personal/components/ResumeInfoDetailModal.vue

@@ -24,10 +24,10 @@
       <a-divider orientation="left" orientation-margin="0px">求职意向</a-divider>
       <a-descriptions :column="3" bordered size="middle" style="margin-bottom: 24px">
         <a-descriptions-item label="期望行业" :span="1">
-          {{ detailData.expectedIndustry }}
+          {{ getDictText('EconomicIndustry', detailData.expectedIndustry) }}
         </a-descriptions-item>
         <a-descriptions-item label="期望职位" :span="1">
-          {{ detailData.expectedPosition }}
+          {{ positionLabel || detailData.expectedPosition }}
         </a-descriptions-item>
         <a-descriptions-item label="职位名称" :span="1">
           {{ detailData.positionName }}
@@ -67,6 +67,12 @@
           <template v-else-if="column.dataIndex === 'companySize'">
             {{ getDictText('CompanySize', text) }}
           </template>
+          <template v-else-if="column.dataIndex === 'industry'">
+            {{ getDictText('EconomicIndustry', text) }}
+          </template>
+          <template v-else-if="column.dataIndex === 'economyType'">
+            {{ getDictText('EconomicType', text) }}
+          </template>
         </template>
       </a-table>
 
@@ -178,11 +184,14 @@
   import { defineExpose, ref } from 'vue';
   import JModal from '/@/components/Modal/src/JModal/JModal.vue';
   import { useDict } from '/@/hooks/dictionary/useDict';
+  import { defHttp } from '/@/utils/http/axios';
   import { queryResumeById } from '../ResumeInfo.api';
 
   const visible = ref<boolean>(false);
   const loading = ref<boolean>(false);
   const detailData = ref<Record<string, any>>({});
+  // 期望职位(ZYFL)懒加载字典的 label 文本
+  const positionLabel = ref<string>('');
 
   // 预加载详情展示所需的字典,与编辑表单保持一致
   const { getDictText } = useDict([
@@ -194,6 +203,8 @@
     'EduSystem',
     'SkillLevel',
     'TitleLevel',
+    'EconomicIndustry',
+    'EconomicType',
   ]);
 
   // 工作经历表格列
@@ -279,9 +290,18 @@
   async function open(record: Record<string, any>) {
     visible.value = true;
     loading.value = true;
+    positionLabel.value = '';
     try {
       const res = await queryResumeById({ id: record.id });
       detailData.value = res || {};
+      // 期望职位(ZYFL)数据量大,懒加载无法用 getDictText,调 API 获取名称
+      if (detailData.value.expectedPosition) {
+        const apiRes = await defHttp.get({
+          url: `/system/dictionary/loadDictItem/ZYFL`,
+          params: { key: detailData.value.expectedPosition },
+        }, { isTransformResponse: false });
+        positionLabel.value = apiRes.success && apiRes.result?.length ? apiRes.result[0] : '';
+      }
     } finally {
       loading.value = false;
     }

+ 599 - 236
jeecgboot-vue3/src/views/recruitment/personal/components/ResumeInfoForm.vue

@@ -4,42 +4,32 @@
       <template #detail>
         <a-form ref="formRef" :labelCol="labelCol" :wrapperCol="wrapperCol" class="antd-modal-form" name="ResumeInfoForm">
           <a-row :gutter="24">
-            <a-col :span="8">
-              <a-form-item id="ResumeInfoForm-resumeName" label="简历名称" name="resumeName" v-bind="validateInfos.resumeName">
-                <a-input v-model:value="formData.resumeName" allow-clear placeholder="请输入简历名称"></a-input>
-              </a-form-item>
-            </a-col>
-            <a-col :span="8">
-              <a-form-item id="ResumeInfoForm-validDate" label="有效期" name="validDate" v-bind="validateInfos.validDate">
-                <a-date-picker v-model:value="formData.validDate" allow-clear placeholder="请选择有效期" style="width: 100%" />
-              </a-form-item>
-            </a-col>
-            <a-col :span="8">
-              <a-form-item id="ResumeInfoForm-isPublic" label="是否公开" name="isPublic" v-bind="validateInfos.isPublic">
-                <a-radio-group v-model:value="formData.isPublic">
-                  <a-radio value="1">是</a-radio>
-                  <a-radio value="0">否</a-radio>
-                </a-radio-group>
-              </a-form-item>
-            </a-col>
-            <a-col :span="8">
-              <a-form-item id="ResumeInfoForm-isDefault" label="是否默认简历" name="isDefault" v-bind="validateInfos.isDefault">
-                <a-radio-group v-model:value="formData.isDefault">
-                  <a-radio value="1">是</a-radio>
-                  <a-radio value="0">否</a-radio>
-                </a-radio-group>
-              </a-form-item>
-            </a-col>
             <a-divider orientation="left" orientation-margin="10px">求职意向</a-divider>
             <!-- 求职意向字段(原子表数据合并入主表) -->
             <a-col :span="8">
               <a-form-item id="ResumeInfoForm-expectedIndustry" label="期望行业" name="expectedIndustry" v-bind="validateInfos.expectedIndustry">
-                <a-input v-model:value="formData.expectedIndustry" allow-clear placeholder="请输入期望行业"></a-input>
+                <a-tree-select
+                  v-model:value="expectedIndustryValueObj"
+                  :tree-data="expectedIndustryTreeData"
+                  placeholder="请选择期望行业"
+                  allow-clear
+                  label-in-value
+                  @change="(val) => { formData.expectedIndustry = val?.value || ''; }"
+                />
               </a-form-item>
             </a-col>
             <a-col :span="8">
               <a-form-item id="ResumeInfoForm-expectedPosition" label="期望职位" name="expectedPosition" v-bind="validateInfos.expectedPosition">
-                <a-input v-model:value="formData.expectedPosition" allow-clear placeholder="请输入期望职位"></a-input>
+                <a-tree-select
+                  v-model:value="positionValueObj"
+                  v-model:tree-expanded-keys="positionExpandedKeys"
+                  :tree-data="positionTreeData"
+                  :load-data="(node) => loadPositionChildren(node)"
+                  placeholder="请选择期望职位"
+                  allow-clear
+                  label-in-value
+                  @change="(val) => { formData.expectedPosition = val?.value || ''; }"
+                />
               </a-form-item>
             </a-col>
             <a-col :span="8">
@@ -81,152 +71,174 @@
               </a-form-item>
             </a-col>
           </a-row>
+
           <!-- 工作经历(子表) -->
           <a-divider orientation="left" orientation-margin="0px">工作经历</a-divider>
-          <JVxeTable
-            ref="workExpTableRef"
-            :columns="computedWorkExpColumns"
+          <div style="text-align: right; margin-bottom: 8px;">
+            <a-button v-if="!disabled" type="primary" size="small" @click="openWorkExpModal()">
+              <template #icon><plus-outlined /></template>
+              新增
+            </a-button>
+          </div>
+          <a-table
+            :columns="workExpColumns"
             :dataSource="workExpData.dataSource"
-            :height="250"
-            keepSource
-            rowNumber
-            rowSelection
-            stripe
-            toolbar
+            :pagination="false"
+            bordered
+            size="small"
+            rowKey="id"
+            style="margin-bottom: 24px"
           >
-            <template #companyTypeSlot="{ row, triggerChange }">
-              <a-select
-                :bordered="false"
-                :options="companyTypeOptions"
-                :value="row.companyType"
-                allow-clear
-                placeholder="请选择单位类型"
-                style="width: 100%"
-                @change="(val) => triggerChange(val)"
-              />
-            </template>
-            <template #companySizeSlot="{ row, triggerChange }">
-              <a-select
-                :bordered="false"
-                :options="companySizeOptions"
-                :value="row.companySize"
-                allow-clear
-                placeholder="请选择人员规模"
-                style="width: 100%"
-                @change="(val) => triggerChange(val)"
-              />
+            <template #bodyCell="{ column, text, record, index }">
+              <template v-if="column.dataIndex === 'companyType'">
+                {{ getDictText('CompanyType', text) }}
+              </template>
+              <template v-else-if="column.dataIndex === 'companySize'">
+                {{ getDictText('CompanySize', text) }}
+              </template>
+              <template v-else-if="column.dataIndex === 'industry'">
+                {{ getDictText('EconomicIndustry', text) }}
+              </template>
+              <template v-else-if="column.dataIndex === 'economyType'">
+                {{ getDictText('EconomicType', text) }}
+              </template>
+              <template v-if="column.dataIndex === 'action' && !disabled">
+                <a @click="openWorkExpModal(record, index)">编辑</a>
+                <a-divider type="vertical" />
+                <a-popconfirm title="确定删除吗?" @confirm="removeWorkExp(index)">
+                  <a style="color: #ff4d4f">删除</a>
+                </a-popconfirm>
+              </template>
             </template>
-          </JVxeTable>
+          </a-table>
+
           <!-- 教育经历(子表) -->
           <a-divider orientation="left" orientation-margin="0px">教育经历</a-divider>
-          <JVxeTable
-            ref="educationTableRef"
-            :columns="computedEducationColumns"
+          <div style="text-align: right; margin-bottom: 8px;">
+            <a-button v-if="!disabled" type="primary" size="small" @click="openEducationModal()">
+              <template #icon><plus-outlined /></template>
+              新增
+            </a-button>
+          </div>
+          <a-table
+            :columns="educationColumns"
             :dataSource="educationData.dataSource"
-            :height="250"
-            keepSource
-            rowNumber
-            rowSelection
-            stripe
-            toolbar
+            :pagination="false"
+            bordered
+            size="small"
+            rowKey="id"
+            style="margin-bottom: 24px"
           >
-            <template #educationSlot="{ row, triggerChange }">
-              <a-select
-                :bordered="false"
-                :options="educationOptions"
-                :value="row.education"
-                allow-clear
-                placeholder="请选择学历"
-                style="width: 100%"
-                @change="(val) => triggerChange(val)"
-              />
-            </template>
-            <template #degreeSlot="{ row, triggerChange }">
-              <a-select
-                :bordered="false"
-                :options="degreeOptions"
-                :value="row.degree"
-                allow-clear
-                placeholder="请选择学位"
-                style="width: 100%"
-                @change="(val) => triggerChange(val)"
-              />
+            <template #bodyCell="{ column, text, record, index }">
+              <template v-if="column.dataIndex === 'education'">
+                {{ getDictText('Education', text) }}
+              </template>
+              <template v-else-if="column.dataIndex === 'degree'">
+                {{ getDictText('Degree', text) }}
+              </template>
+              <template v-else-if="column.dataIndex === 'eduSystem'">
+                {{ getDictText('EduSystem', text) }}
+              </template>
+              <template v-if="column.dataIndex === 'action' && !disabled">
+                <a @click="openEducationModal(record, index)">编辑</a>
+                <a-divider type="vertical" />
+                <a-popconfirm title="确定删除吗?" @confirm="removeEducation(index)">
+                  <a style="color: #ff4d4f">删除</a>
+                </a-popconfirm>
+              </template>
             </template>
-            <template #eduSystemSlot="{ row, triggerChange }">
-              <a-select
-                :bordered="false"
-                :options="eduSystemOptions"
-                :value="row.eduSystem"
-                allow-clear
-                placeholder="请选择学制"
-                style="width: 100%"
-                @change="(val) => triggerChange(val)"
-              />
-            </template>
-          </JVxeTable>
+          </a-table>
+
           <!-- 培训经历(子表) -->
           <a-divider orientation="left" orientation-margin="0px">培训经历</a-divider>
-          <JVxeTable
-            ref="trainingTableRef"
-            :columns="computedTrainingColumns"
+          <div style="text-align: right; margin-bottom: 8px;">
+            <a-button v-if="!disabled" type="primary" size="small" @click="openTrainingModal()">
+              <template #icon><plus-outlined /></template>
+              新增
+            </a-button>
+          </div>
+          <a-table
+            :columns="trainingColumns"
             :dataSource="trainingData.dataSource"
-            :height="250"
-            keepSource
-            rowNumber
-            rowSelection
-            stripe
-            toolbar
-          />
+            :pagination="false"
+            bordered
+            size="small"
+            rowKey="id"
+            style="margin-bottom: 24px"
+          >
+            <template #bodyCell="{ column, record, index }">
+              <template v-if="column.dataIndex === 'action' && !disabled">
+                <a @click="openTrainingModal(record, index)">编辑</a>
+                <a-divider type="vertical" />
+                <a-popconfirm title="确定删除吗?" @confirm="removeTraining(index)">
+                  <a style="color: #ff4d4f">删除</a>
+                </a-popconfirm>
+              </template>
+            </template>
+          </a-table>
+
           <!-- 职业技能(子表) -->
           <a-divider orientation="left" orientation-margin="0px">职业技能</a-divider>
-          <JVxeTable
-            ref="skillTableRef"
-            :columns="computedSkillColumns"
+          <div style="text-align: right; margin-bottom: 8px;">
+            <a-button v-if="!disabled" type="primary" size="small" @click="openSkillModal()">
+              <template #icon><plus-outlined /></template>
+              新增
+            </a-button>
+          </div>
+          <a-table
+            :columns="skillColumns"
             :dataSource="skillData.dataSource"
-            :height="250"
-            keepSource
-            rowNumber
-            rowSelection
-            stripe
-            toolbar
+            :pagination="false"
+            bordered
+            size="small"
+            rowKey="id"
+            style="margin-bottom: 24px"
           >
-            <template #skillLevelSlot="{ row, triggerChange }">
-              <a-select
-                :bordered="false"
-                :options="skillLevelOptions"
-                :value="row.skillLevel"
-                allow-clear
-                placeholder="请选择技能等级"
-                style="width: 100%"
-                @change="(val) => triggerChange(val)"
-              />
+            <template #bodyCell="{ column, text, record, index }">
+              <template v-if="column.dataIndex === 'skillLevel'">
+                {{ getDictText('SkillLevel', text) }}
+              </template>
+              <template v-if="column.dataIndex === 'action' && !disabled">
+                <a @click="openSkillModal(record, index)">编辑</a>
+                <a-divider type="vertical" />
+                <a-popconfirm title="确定删除吗?" @confirm="removeSkill(index)">
+                  <a style="color: #ff4d4f">删除</a>
+                </a-popconfirm>
+              </template>
             </template>
-          </JVxeTable>
+          </a-table>
+
           <!-- 职称情况(子表) -->
           <a-divider orientation="left" orientation-margin="0px">职称情况</a-divider>
-          <JVxeTable
-            ref="titleTableRef"
-            :columns="computedTitleColumns"
+          <div style="text-align: right; margin-bottom: 8px;">
+            <a-button v-if="!disabled" type="primary" size="small" @click="openTitleModal()">
+              <template #icon><plus-outlined /></template>
+              新增
+            </a-button>
+          </div>
+          <a-table
+            :columns="titleColumns"
             :dataSource="titleData.dataSource"
-            :height="250"
-            keepSource
-            rowNumber
-            rowSelection
-            stripe
-            toolbar
+            :pagination="false"
+            bordered
+            size="small"
+            rowKey="id"
+            style="margin-bottom: 24px"
           >
-            <template #titleLevelSlot="{ row, triggerChange }">
-              <a-select
-                :bordered="false"
-                :options="titleLevelOptions"
-                :value="row.level"
-                allow-clear
-                placeholder="请选择职称级别"
-                style="width: 100%"
-                @change="(val) => triggerChange(val)"
-              />
+            <template #bodyCell="{ column, text, record, index }">
+              <template v-if="column.dataIndex === 'level'">
+                {{ getDictText('TitleLevel', text) }}
+              </template>
+              <template v-if="column.dataIndex === 'action' && !disabled">
+                <a @click="openTitleModal(record, index)">编辑</a>
+                <a-divider type="vertical" />
+                <a-popconfirm title="确定删除吗?" @confirm="removeTitle(index)">
+                  <a style="color: #ff4d4f">删除</a>
+                </a-popconfirm>
+              </template>
             </template>
-          </JVxeTable>
+          </a-table>
+
           <a-divider orientation="left" orientation-margin="0px">其他证书</a-divider>
           <a-row :gutter="24">
             <a-col :span="24">
@@ -242,35 +254,74 @@
               </a-form-item>
             </a-col>
           </a-row>
+
           <!-- 获奖情况(子表) -->
           <a-divider orientation="left" orientation-margin="0px">获奖情况</a-divider>
-          <JVxeTable
-            ref="awardTableRef"
-            :columns="computedAwardColumns"
+          <div style="text-align: right; margin-bottom: 8px;">
+            <a-button v-if="!disabled" type="primary" size="small" @click="openAwardModal()">
+              <template #icon><plus-outlined /></template>
+              新增
+            </a-button>
+          </div>
+          <a-table
+            :columns="awardColumns"
             :dataSource="awardData.dataSource"
-            :height="250"
-            keepSource
-            rowNumber
-            rowSelection
-            stripe
-            toolbar
-          />
+            :pagination="false"
+            bordered
+            size="small"
+            rowKey="id"
+            style="margin-bottom: 24px"
+          >
+            <template #bodyCell="{ column, record, index }">
+              <template v-if="column.dataIndex === 'action' && !disabled">
+                <a @click="openAwardModal(record, index)">编辑</a>
+                <a-divider type="vertical" />
+                <a-popconfirm title="确定删除吗?" @confirm="removeAward(index)">
+                  <a style="color: #ff4d4f">删除</a>
+                </a-popconfirm>
+              </template>
+            </template>
+          </a-table>
+
           <!-- 留学经历(子表) -->
           <a-divider orientation="left" orientation-margin="0px">留学经历</a-divider>
-          <JVxeTable
-            ref="studyAbroadTableRef"
-            :columns="computedStudyAbroadColumns"
+          <div style="text-align: right; margin-bottom: 8px;">
+            <a-button v-if="!disabled" type="primary" size="small" @click="openStudyAbroadModal()">
+              <template #icon><plus-outlined /></template>
+              新增
+            </a-button>
+          </div>
+          <a-table
+            :columns="studyAbroadColumns"
             :dataSource="studyAbroadData.dataSource"
-            :height="250"
-            keepSource
-            rowNumber
-            rowSelection
-            stripe
-            toolbar
-          />
+            :pagination="false"
+            bordered
+            size="small"
+            rowKey="id"
+            style="margin-bottom: 24px"
+          >
+            <template #bodyCell="{ column, record, index }">
+              <template v-if="column.dataIndex === 'action' && !disabled">
+                <a @click="openStudyAbroadModal(record, index)">编辑</a>
+                <a-divider type="vertical" />
+                <a-popconfirm title="确定删除吗?" @confirm="removeStudyAbroad(index)">
+                  <a style="color: #ff4d4f">删除</a>
+                </a-popconfirm>
+              </template>
+            </template>
+          </a-table>
         </a-form>
       </template>
     </JFormContainer>
+
+    <!-- 子表新增/编辑弹窗 -->
+    <ResumeWorkExpModal ref="workExpModalRef" @success="onWorkExpSuccess" />
+    <ResumeEducationModal ref="educationModalRef" @success="onEducationSuccess" />
+    <ResumeTrainingModal ref="trainingModalRef" @success="onTrainingSuccess" />
+    <ResumeSkillModal ref="skillModalRef" @success="onSkillSuccess" />
+    <ResumeTitleModal ref="titleModalRef" @success="onTitleSuccess" />
+    <ResumeAwardModal ref="awardModalRef" @success="onAwardSuccess" />
+    <ResumeStudyAbroadModal ref="studyAbroadModalRef" @success="onStudyAbroadSuccess" />
   </a-spin>
 </template>
 
@@ -278,21 +329,22 @@
   import { computed, defineExpose, nextTick, reactive, ref } from 'vue';
   import { useMessage } from '/@/hooks/web/useMessage';
   import { useDict } from '/@/hooks/dictionary/useDict';
+  import { defHttp } from '/@/utils/http/axios';
   import { getDateByPicker, getValueType } from '/@/utils';
   import { editResume, queryResumeById, saveResume } from '../ResumeInfo.api';
   import dayjs from 'dayjs';
-  import {
-    awardColumns,
-    educationColumns,
-    skillColumns,
-    studyAbroadColumns,
-    titleColumns,
-    trainingColumns,
-    workExpColumns,
-  } from '../ResumeInfo.data';
-  import { JVxeColumn } from '/@/components/jeecg/JVxeTable/types';
   import { Form } from 'ant-design-vue';
   import JFormContainer from '/@/components/Form/src/container/JFormContainer.vue';
+  import { PlusOutlined } from '@ant-design/icons-vue';
+
+  // 子表弹窗组件
+  import ResumeWorkExpModal from './ResumeWorkExpModal.vue';
+  import ResumeEducationModal from './ResumeEducationModal.vue';
+  import ResumeTrainingModal from './ResumeTrainingModal.vue';
+  import ResumeSkillModal from './ResumeSkillModal.vue';
+  import ResumeTitleModal from './ResumeTitleModal.vue';
+  import ResumeAwardModal from './ResumeAwardModal.vue';
+  import ResumeStudyAbroadModal from './ResumeStudyAbroadModal.vue';
 
   const props = defineProps({
     formDisabled: { type: Boolean, default: false },
@@ -336,24 +388,96 @@
   const fieldPickers = reactive({});
 
   // 预加载字典数据
-  const { getDictOptions } = useDict(['WorkNature', 'CompanyType', 'CompanySize', 'Education', 'Degree', 'EduSystem', 'SkillLevel', 'TitleLevel']);
+  const { getDictOptions, getDictText } = useDict(['WorkNature', 'CompanyType', 'CompanySize', 'Education', 'Degree', 'EduSystem', 'SkillLevel', 'TitleLevel', 'EconomicIndustry', 'EconomicType']);
   const workNatureOptions = computed(() => getDictOptions('WorkNature'));
-  const companyTypeOptions = computed(() => getDictOptions('CompanyType'));
-  const companySizeOptions = computed(() => getDictOptions('CompanySize'));
-  const educationOptions = computed(() => getDictOptions('Education'));
-  const degreeOptions = computed(() => getDictOptions('Degree'));
-  const eduSystemOptions = computed(() => getDictOptions('EduSystem'));
-  const skillLevelOptions = computed(() => getDictOptions('SkillLevel'));
-  const titleLevelOptions = computed(() => getDictOptions('TitleLevel'));
-
-  const workExpTableRef = ref();
-  const educationTableRef = ref();
-  const trainingTableRef = ref();
-  const skillTableRef = ref();
-  const titleTableRef = ref();
-  const awardTableRef = ref();
-  const studyAbroadTableRef = ref();
 
+  // ---------- 树形字典:期望行业 (EconomicIndustry) ----------
+  const expectedIndustryTreeData = ref<any[]>([]);
+  const expectedIndustryValueObj = ref<any>(undefined);
+
+  // ---------- 树形字典:期望职位 (ZYFL) ----------
+  const positionTreeData = ref<any[]>([]);
+  const positionValueObj = ref<any>(undefined);
+  const positionExpandedKeys = ref<string[]>([]);
+
+  /**
+   * 从 getDictTree 接口加载树形字典完整数据
+   * 后端返回格式: { label, value, children }
+   * 映射为 a-tree-select 识别的: { key, title, value, isLeaf, children }
+   */
+  async function loadTreeData(dictionaryCode: string) {
+    const res = await defHttp.get({ url: '/system/dictionary/getDictTree', params: { dictionaryCode } }, { isTransformResponse: false });
+    if (res.success && res.result) {
+      return res.result.map((item) => treeNodeMapper(item));
+    }
+    return [];
+  }
+
+  /** 递归映射树节点:从后端 {label, value, children} 转为 {key, title, value, isLeaf, children} */
+  function treeNodeMapper(node: any): any {
+    return {
+      key: String(node.value),
+      title: node.label,
+      value: String(node.value),
+      isLeaf: !node.children || node.children.length === 0,
+      children: node.children?.length ? node.children.map((child) => treeNodeMapper(child)) : undefined,
+    };
+  }
+
+  /** 在树形字典数据中递归查找 value 对应的 label */
+  function findTreeNodeLabel(tree: any[], value: string): string | undefined {
+    for (const node of tree) {
+      if (node.value === value) return node.title;
+      if (node.children) {
+        const found = findTreeNodeLabel(node.children, value);
+        if (found) return found;
+      }
+    }
+    return undefined;
+  }
+
+  /**
+   * 加载期望职位(ZYFL)树形字典根节点(懒加载,数据量2000+)
+   */
+  async function loadPositionRootNodes() {
+    const res = await defHttp.get({
+      url: '/system/dictionary/getDictTreeChildren',
+      params: { dictionaryCode: 'ZYFL', parentCode: '' },
+    }, { isTransformResponse: false });
+    if (res.success && res.result) {
+      return res.result.map((item) => ({
+        key: item.key,
+        title: item.title,
+        value: item.key,
+        isLeaf: item.leaf,
+      }));
+    }
+    return [];
+  }
+
+  /**
+   * 懒加载期望职位(ZYFL)子节点(展开时按需加载下一级)
+   */
+  async function loadPositionChildren(treeNode) {
+    if (treeNode.dataRef.children) return Promise.resolve();
+    const res = await defHttp.get({
+      url: '/system/dictionary/getDictTreeChildren',
+      params: { dictionaryCode: 'ZYFL', parentCode: treeNode.value },
+    }, { isTransformResponse: false });
+    if (res.success && res.result) {
+      const children = res.result.map((item) => ({
+        key: item.key,
+        title: item.title,
+        value: item.key,
+        isLeaf: item.leaf,
+      }));
+      treeNode.dataRef.children = children;
+      positionTreeData.value = [...positionTreeData.value];
+    }
+    return Promise.resolve();
+  }
+
+  // 子表数据源
   const workExpData = reactive({ dataSource: [] });
   const educationData = reactive({ dataSource: [] });
   const trainingData = reactive({ dataSource: [] });
@@ -362,6 +486,24 @@
   const awardData = reactive({ dataSource: [] });
   const studyAbroadData = reactive({ dataSource: [] });
 
+  // 编辑时记录当前操作的行索引
+  const editingWorkExpIndex = ref<number | null>(null);
+  const editingEducationIndex = ref<number | null>(null);
+  const editingTrainingIndex = ref<number | null>(null);
+  const editingSkillIndex = ref<number | null>(null);
+  const editingTitleIndex = ref<number | null>(null);
+  const editingAwardIndex = ref<number | null>(null);
+  const editingStudyAbroadIndex = ref<number | null>(null);
+
+  // 子表弹窗引用
+  const workExpModalRef = ref();
+  const educationModalRef = ref();
+  const trainingModalRef = ref();
+  const skillModalRef = ref();
+  const titleModalRef = ref();
+  const awardModalRef = ref();
+  const studyAbroadModalRef = ref();
+
   const disabled = computed(() => {
     if (props.formBpm === true) {
       if (props.formData.disabled === false) {
@@ -373,33 +515,230 @@
     return props.formDisabled;
   });
 
-  function applyDisabled(columns: JVxeColumn[]): JVxeColumn[] {
-    if (!disabled.value) return columns;
-    return columns.map((col) => ({ ...col, disabled: true }));
-  }
-
-  const computedWorkExpColumns = computed(() => applyDisabled(workExpColumns));
-  const computedEducationColumns = computed(() => applyDisabled(educationColumns));
-  const computedTrainingColumns = computed(() => applyDisabled(trainingColumns));
-  const computedSkillColumns = computed(() => applyDisabled(skillColumns));
-  const computedTitleColumns = computed(() => applyDisabled(titleColumns));
-  const computedAwardColumns = computed(() => applyDisabled(awardColumns));
-  const computedStudyAbroadColumns = computed(() => applyDisabled(studyAbroadColumns));
-
-  function convertDateFields(list: any[], dateKeys: string[]): any[] {
-    if (!list || list.length === 0) return list;
-    return list.map((item) => {
-      const newItem = { ...item };
-      dateKeys.forEach((key) => {
-        if (newItem[key] && typeof newItem[key] === 'string') {
-          newItem[key] = dayjs(newItem[key]);
-        }
-      });
-      return newItem;
-    });
+  // ---------- 表格列定义 ----------
+  const workExpColumns = [
+    { title: '工作单位', dataIndex: 'companyName', width: 180 },
+    { title: '所在职位', dataIndex: 'position', width: 150 },
+    { title: '开始时间', dataIndex: 'startDate', width: 120 },
+    { title: '结束时间', dataIndex: 'endDate', width: 120 },
+    { title: '所属行业', dataIndex: 'industry', width: 150 },
+    { title: '单位类型', dataIndex: 'companyType', width: 120 },
+    { title: '经济类型', dataIndex: 'economyType', width: 120 },
+    { title: '人员规模', dataIndex: 'companySize', width: 120 },
+    { title: '工作职责', dataIndex: 'workResponsibility', width: 200 },
+    { title: '工作业绩', dataIndex: 'workAchievement', width: 200 },
+    { title: '操作', dataIndex: 'action', width: 100, fixed: 'right' },
+  ];
+
+  const educationColumns = [
+    { title: '开始时间', dataIndex: 'startDate', width: 120 },
+    { title: '结束时间', dataIndex: 'endDate', width: 120 },
+    { title: '毕业院校', dataIndex: 'schoolName', width: 180 },
+    { title: '所学专业', dataIndex: 'major', width: 150 },
+    { title: '学历', dataIndex: 'education', width: 100 },
+    { title: '学位', dataIndex: 'degree', width: 100 },
+    { title: '学制', dataIndex: 'eduSystem', width: 100 },
+    { title: '操作', dataIndex: 'action', width: 100, fixed: 'right' },
+  ];
+
+  const trainingColumns = [
+    { title: '培训机构', dataIndex: 'trainingInstitution', width: 180 },
+    { title: '培训课程', dataIndex: 'trainingCourse', width: 150 },
+    { title: '开始时间', dataIndex: 'startDate', width: 120 },
+    { title: '结束时间', dataIndex: 'endDate', width: 120 },
+    { title: '资格证书号', dataIndex: 'certificateNo', width: 180 },
+    { title: '操作', dataIndex: 'action', width: 100, fixed: 'right' },
+  ];
+
+  const skillColumns = [
+    { title: '职业名称', dataIndex: 'professionName', width: 180 },
+    { title: '工种/职业方向', dataIndex: 'workType', width: 150 },
+    { title: '技能等级', dataIndex: 'skillLevel', width: 120 },
+    { title: '发证日期', dataIndex: 'issueDate', width: 120 },
+    { title: '发证机构', dataIndex: 'issueOrganization', width: 180 },
+    { title: '证书编号', dataIndex: 'certificateNo', width: 180 },
+    { title: '操作', dataIndex: 'action', width: 100, fixed: 'right' },
+  ];
+
+  const titleColumns = [
+    { title: '职称名称', dataIndex: 'titleName', width: 180 },
+    { title: '专业', dataIndex: 'profession', width: 150 },
+    { title: '级别', dataIndex: 'level', width: 100 },
+    { title: '发证日期', dataIndex: 'issueDate', width: 120 },
+    { title: '发证单位', dataIndex: 'issueUnit', width: 180 },
+    { title: '证书编号', dataIndex: 'certificateNo', width: 180 },
+    { title: '操作', dataIndex: 'action', width: 100, fixed: 'right' },
+  ];
+
+  const awardColumns = [
+    { title: '证书名称', dataIndex: 'certificateName', width: 180 },
+    { title: '发证日期', dataIndex: 'issueDate', width: 120 },
+    { title: '颁奖机构', dataIndex: 'awardOrganization', width: 180 },
+    { title: '角色', dataIndex: 'role', width: 100 },
+    { title: '成果名称', dataIndex: 'achievementName', width: 180 },
+    { title: '成果说明', dataIndex: 'achievementDesc', width: 200 },
+    { title: '其他说明', dataIndex: 'otherDesc', width: 200 },
+    { title: '操作', dataIndex: 'action', width: 100, fixed: 'right' },
+  ];
+
+  const studyAbroadColumns = [
+    { title: '院校名称', dataIndex: 'schoolName', width: 180 },
+    { title: '国家或地区', dataIndex: 'countryRegion', width: 120 },
+    { title: '留学专业', dataIndex: 'major', width: 150 },
+    { title: '是否毕业', dataIndex: 'isGraduated', width: 100 },
+    { title: '开始时间', dataIndex: 'startDate', width: 120 },
+    { title: '结束时间', dataIndex: 'endDate', width: 120 },
+    { title: '海外学历认证书编号', dataIndex: 'diplomaCertNo', width: 200 },
+    { title: '操作', dataIndex: 'action', width: 100, fixed: 'right' },
+  ];
+
+  // ---------- 子表操作:新增/编辑/删除 ----------
+  let tempIdCounter = 0;
+  function genId(): string {
+    return '__temp_' + ++tempIdCounter;
+  }
+
+  // 工作经历
+  function openWorkExpModal(record?: any, index?: number) {
+    editingWorkExpIndex.value = index !== undefined ? index : null;
+    workExpModalRef.value?.open(record);
+  }
+  function onWorkExpSuccess(data: any) {
+    if (editingWorkExpIndex.value !== null) {
+      workExpData.dataSource[editingWorkExpIndex.value] = data;
+    } else {
+      data.id = genId();
+      workExpData.dataSource.push(data);
+    }
+    workExpData.dataSource = [...workExpData.dataSource];
+  }
+  function removeWorkExp(index: number) {
+    workExpData.dataSource.splice(index, 1);
+    workExpData.dataSource = [...workExpData.dataSource];
+  }
+
+  // 教育经历
+  function openEducationModal(record?: any, index?: number) {
+    editingEducationIndex.value = index !== undefined ? index : null;
+    educationModalRef.value?.open(record);
+  }
+  function onEducationSuccess(data: any) {
+    if (editingEducationIndex.value !== null) {
+      educationData.dataSource[editingEducationIndex.value] = data;
+    } else {
+      data.id = genId();
+      educationData.dataSource.push(data);
+    }
+    educationData.dataSource = [...educationData.dataSource];
+  }
+  function removeEducation(index: number) {
+    educationData.dataSource.splice(index, 1);
+    educationData.dataSource = [...educationData.dataSource];
+  }
+
+  // 培训经历
+  function openTrainingModal(record?: any, index?: number) {
+    editingTrainingIndex.value = index !== undefined ? index : null;
+    trainingModalRef.value?.open(record);
+  }
+  function onTrainingSuccess(data: any) {
+    if (editingTrainingIndex.value !== null) {
+      trainingData.dataSource[editingTrainingIndex.value] = data;
+    } else {
+      data.id = genId();
+      trainingData.dataSource.push(data);
+    }
+    trainingData.dataSource = [...trainingData.dataSource];
+  }
+  function removeTraining(index: number) {
+    trainingData.dataSource.splice(index, 1);
+    trainingData.dataSource = [...trainingData.dataSource];
+  }
+
+  // 职业技能
+  function openSkillModal(record?: any, index?: number) {
+    editingSkillIndex.value = index !== undefined ? index : null;
+    skillModalRef.value?.open(record);
+  }
+  function onSkillSuccess(data: any) {
+    if (editingSkillIndex.value !== null) {
+      skillData.dataSource[editingSkillIndex.value] = data;
+    } else {
+      data.id = genId();
+      skillData.dataSource.push(data);
+    }
+    skillData.dataSource = [...skillData.dataSource];
+  }
+  function removeSkill(index: number) {
+    skillData.dataSource.splice(index, 1);
+    skillData.dataSource = [...skillData.dataSource];
+  }
+
+  // 职称情况
+  function openTitleModal(record?: any, index?: number) {
+    editingTitleIndex.value = index !== undefined ? index : null;
+    titleModalRef.value?.open(record);
+  }
+  function onTitleSuccess(data: any) {
+    if (editingTitleIndex.value !== null) {
+      titleData.dataSource[editingTitleIndex.value] = data;
+    } else {
+      data.id = genId();
+      titleData.dataSource.push(data);
+    }
+    titleData.dataSource = [...titleData.dataSource];
+  }
+  function removeTitle(index: number) {
+    titleData.dataSource.splice(index, 1);
+    titleData.dataSource = [...titleData.dataSource];
+  }
+
+  // 获奖情况
+  function openAwardModal(record?: any, index?: number) {
+    editingAwardIndex.value = index !== undefined ? index : null;
+    awardModalRef.value?.open(record);
+  }
+  function onAwardSuccess(data: any) {
+    if (editingAwardIndex.value !== null) {
+      awardData.dataSource[editingAwardIndex.value] = data;
+    } else {
+      data.id = genId();
+      awardData.dataSource.push(data);
+    }
+    awardData.dataSource = [...awardData.dataSource];
+  }
+  function removeAward(index: number) {
+    awardData.dataSource.splice(index, 1);
+    awardData.dataSource = [...awardData.dataSource];
   }
 
-  function add(personalId) {
+  // 留学经历
+  function openStudyAbroadModal(record?: any, index?: number) {
+    editingStudyAbroadIndex.value = index !== undefined ? index : null;
+    studyAbroadModalRef.value?.open(record);
+  }
+  function onStudyAbroadSuccess(data: any) {
+    if (editingStudyAbroadIndex.value !== null) {
+      studyAbroadData.dataSource[editingStudyAbroadIndex.value] = data;
+    } else {
+      data.id = genId();
+      studyAbroadData.dataSource.push(data);
+    }
+    studyAbroadData.dataSource = [...studyAbroadData.dataSource];
+  }
+  function removeStudyAbroad(index: number) {
+    studyAbroadData.dataSource.splice(index, 1);
+    studyAbroadData.dataSource = [...studyAbroadData.dataSource];
+  }
+
+  // ---------- 主表操作 ----------
+  async function add(personalId) {
+    // 加载树形字典数据:期望行业全量加载,期望职位懒加载
+    expectedIndustryTreeData.value = await loadTreeData('EconomicIndustry');
+    expectedIndustryValueObj.value = undefined;
+    positionTreeData.value = await loadPositionRootNodes();
+    positionValueObj.value = undefined;
+    positionExpandedKeys.value = [];
     edit({ personalId });
   }
 
@@ -407,6 +746,10 @@
     await nextTick();
     resetFields();
     resetSubTables();
+    // 重置树形字典状态
+    expectedIndustryValueObj.value = undefined;
+    positionValueObj.value = undefined;
+    positionExpandedKeys.value = [];
     if (record.id) {
       confirmLoading.value = true;
       try {
@@ -422,13 +765,31 @@
             tmpData.validDate = dayjs(tmpData.validDate);
           }
           Object.assign(formData, tmpData);
-          workExpData.dataSource = convertDateFields(res.resumeWorkExpList || [], ['startDate', 'endDate']);
-          educationData.dataSource = convertDateFields(res.resumeEducationList || [], ['startDate', 'endDate']);
-          trainingData.dataSource = convertDateFields(res.resumeTrainingList || [], ['startDate', 'endDate']);
-          skillData.dataSource = convertDateFields(res.resumeSkillList || [], ['issueDate']);
-          titleData.dataSource = convertDateFields(res.resumeTitleList || [], ['issueDate']);
-          awardData.dataSource = convertDateFields(res.resumeAwardList || [], ['issueDate']);
-          studyAbroadData.dataSource = convertDateFields(res.resumeStudyAbroadList || [], ['startDate', 'endDate']);
+          // 加载树形字典数据:期望行业全量加载,期望职位懒加载
+          expectedIndustryTreeData.value = await loadTreeData('EconomicIndustry');
+          positionTreeData.value = await loadPositionRootNodes();
+          // 期望行业:从已加载的树数据中查找 label 回显
+          if (tmpData.expectedIndustry) {
+            const label = findTreeNodeLabel(expectedIndustryTreeData.value, tmpData.expectedIndustry) || tmpData.expectedIndustry;
+            expectedIndustryValueObj.value = { value: tmpData.expectedIndustry, label };
+          }
+          // 期望职位:调 API 获取字典项名称用于编辑回显
+          if (tmpData.expectedPosition) {
+            const apiRes = await defHttp.get({
+              url: `/system/dictionary/loadDictItem/ZYFL`,
+              params: { key: tmpData.expectedPosition },
+            }, { isTransformResponse: false });
+            const label = apiRes.success && apiRes.result?.length ? apiRes.result[0] : tmpData.expectedPosition;
+            positionValueObj.value = { value: tmpData.expectedPosition, label };
+          }
+          // 直接用原始字符串数据,子表弹窗的 open() 会自行处理 string→dayjs 转换
+          workExpData.dataSource = res.resumeWorkExpList || [];
+          educationData.dataSource = res.resumeEducationList || [];
+          trainingData.dataSource = res.resumeTrainingList || [];
+          skillData.dataSource = res.resumeSkillList || [];
+          titleData.dataSource = res.resumeTitleList || [];
+          awardData.dataSource = res.resumeAwardList || [];
+          studyAbroadData.dataSource = res.resumeStudyAbroadList || [];
         }
       } finally {
         confirmLoading.value = false;
@@ -475,13 +836,15 @@
         }
       }
     }
-    const workExpList = workExpTableRef.value?.getTableData?.() || [];
-    const educationList = educationTableRef.value?.getTableData?.() || [];
-    const trainingList = trainingTableRef.value?.getTableData?.() || [];
-    const skillList = skillTableRef.value?.getTableData?.() || [];
-    const titleList = titleTableRef.value?.getTableData?.() || [];
-    const awardList = awardTableRef.value?.getTableData?.() || [];
-    const studyAbroadList = studyAbroadTableRef.value?.getTableData?.() || [];
+
+    // 从本地 dataSource 收集子表数据
+    const workExpList = [...workExpData.dataSource];
+    const educationList = [...educationData.dataSource];
+    const trainingList = [...trainingData.dataSource];
+    const skillList = [...skillData.dataSource];
+    const titleList = [...titleData.dataSource];
+    const awardList = [...awardData.dataSource];
+    const studyAbroadList = [...studyAbroadData.dataSource];
 
     const dto = {
       ...model,

+ 76 - 3
jeecgboot-vue3/src/views/recruitment/personal/components/ResumeListModal.vue

@@ -36,16 +36,43 @@
     </a-table>
     <ResumeInfoModal ref="resumeInfoModal" @success="loadData" />
     <ResumeInfoDetailModal ref="resumeInfoDetailModal" />
+
+    <!-- 快速新增简历弹窗 -->
+    <j-modal :title="'新增简历'" :visible="quickAddVisible" :width="600" centered @ok="handleQuickAddOk" @cancel="quickAddVisible = false" :confirmLoading="quickAddLoading">
+      <div style="padding: 20px;">
+        <a-form ref="quickAddFormRef" :model="quickAddForm" :labelCol="{ span: 6 }" :wrapperCol="{ span: 16 }">
+          <a-form-item label="简历名称" name="resumeName" :rules="[{ required: true, message: '请输入简历名称' }]">
+            <a-input v-model:value="quickAddForm.resumeName" allow-clear placeholder="请输入简历名称"></a-input>
+          </a-form-item>
+          <a-form-item label="有效期" name="validDate" :rules="[{ required: true, message: '请选择有效期' }]">
+            <a-date-picker v-model:value="quickAddForm.validDate" allow-clear placeholder="请选择有效期" style="width: 100%" />
+          </a-form-item>
+          <a-form-item label="是否公开" name="isPublic">
+            <a-radio-group v-model:value="quickAddForm.isPublic">
+              <a-radio value="1">是</a-radio>
+              <a-radio value="0">否</a-radio>
+            </a-radio-group>
+          </a-form-item>
+          <a-form-item label="是否默认简历" name="isDefault">
+            <a-radio-group v-model:value="quickAddForm.isDefault">
+              <a-radio value="1">是</a-radio>
+              <a-radio value="0">否</a-radio>
+            </a-radio-group>
+          </a-form-item>
+        </a-form>
+      </div>
+    </j-modal>
   </j-modal>
 </template>
 
 <script lang="ts" setup>
-  import { defineExpose, ref } from 'vue';
+  import { defineExpose, ref, reactive } from 'vue';
   import JModal from '/@/components/Modal/src/JModal/JModal.vue';
   import ResumeInfoModal from './ResumeInfoModal.vue';
   import ResumeInfoDetailModal from './ResumeInfoDetailModal.vue';
-  import { deleteResume, resumeList } from '../ResumeInfo.api';
+  import { addBasicResume, deleteResume, resumeList } from '../ResumeInfo.api';
   import { useMessage } from '/@/hooks/web/useMessage';
+  import dayjs from 'dayjs';
 
   const { createMessage } = useMessage();
   const visible = ref<boolean>(false);
@@ -56,6 +83,17 @@
   const resumeInfoModal = ref();
   const resumeInfoDetailModal = ref();
 
+  /** 快速新增弹窗相关 */
+  const quickAddVisible = ref<boolean>(false);
+  const quickAddLoading = ref<boolean>(false);
+  const quickAddFormRef = ref();
+  const quickAddForm = reactive<Record<string, any>>({
+    resumeName: '',
+    validDate: dayjs().add(60, 'day'),
+    isPublic: '1',
+    isDefault: '0',
+  });
+
   const pagination = ref<any>({
     current: 1,
     pageSize: 10,
@@ -135,8 +173,43 @@
     loadData();
   }
 
+  /** 新增:打开快速新增弹窗 */
   function handleAdd() {
-    resumeInfoModal.value.add(personalId.value);
+    // 重置快速新增表单
+    quickAddForm.resumeName = '';
+    quickAddForm.validDate = dayjs().add(60, 'day');
+    quickAddForm.isPublic = '1';
+    quickAddForm.isDefault = '0';
+    quickAddVisible.value = true;
+  }
+
+  /** 快速新增弹窗确认 */
+  async function handleQuickAddOk() {
+    try {
+      await quickAddFormRef.value?.validate();
+    } catch {
+      return;
+    }
+    quickAddLoading.value = true;
+    try {
+      const params = {
+        personalId: personalId.value,
+        resumeName: quickAddForm.resumeName,
+        validDate: quickAddForm.validDate ? dayjs(quickAddForm.validDate).format('YYYY-MM-DD') : null,
+        isPublic: quickAddForm.isPublic,
+        isDefault: quickAddForm.isDefault,
+      };
+      const res = await addBasicResume(params);
+      if (res.success) {
+        createMessage.success(res.message || '添加成功');
+        quickAddVisible.value = false;
+        loadData();
+      } else {
+        createMessage.warning(res.message);
+      }
+    } finally {
+      quickAddLoading.value = false;
+    }
   }
 
   function handleView(record) {

+ 103 - 0
jeecgboot-vue3/src/views/recruitment/personal/components/ResumeSkillModal.vue

@@ -0,0 +1,103 @@
+<template>
+  <j-modal :title="formData.id ? '编辑职业技能' : '新增职业技能'" :visible="visible" :width="700" centered @ok="handleOk" @cancel="handleCancel" :confirmLoading="loading">
+    <div style="padding: 20px;">
+      <a-form ref="formRef" :model="formData" :labelCol="{ span: 6 }" :wrapperCol="{ span: 16 }">
+        <a-row :gutter="24">
+          <a-col :span="12">
+            <a-form-item label="职业名称" name="professionName" :rules="[{ required: true, message: '请输入职业名称' }]">
+              <a-input v-model:value="formData.professionName" allow-clear placeholder="请输入职业名称"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="工种/职业方向" name="workType" :rules="[{ required: true, message: '请输入工种/职业方向' }]">
+              <a-input v-model:value="formData.workType" allow-clear placeholder="请输入工种/职业方向"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="技能等级" name="skillLevel">
+              <a-select v-model:value="formData.skillLevel" :options="skillLevelOptions" allow-clear placeholder="请选择技能等级" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="发证日期" name="issueDate">
+              <a-date-picker v-model:value="formData.issueDate" allow-clear placeholder="请选择发证日期" style="width: 100%" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="发证机构" name="issueOrganization">
+              <a-input v-model:value="formData.issueOrganization" allow-clear placeholder="请输入发证机构"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="证书编号" name="certificateNo">
+              <a-input v-model:value="formData.certificateNo" allow-clear placeholder="请输入证书编号"></a-input>
+            </a-form-item>
+          </a-col>
+        </a-row>
+      </a-form>
+    </div>
+  </j-modal>
+</template>
+
+<script lang="ts" setup>
+  import { defineExpose, reactive, ref } from 'vue';
+  import JModal from '/@/components/Modal/src/JModal/JModal.vue';
+  import { useDict } from '/@/hooks/dictionary/useDict';
+  import { computed } from 'vue';
+  import dayjs from 'dayjs';
+
+  const { getDictOptions } = useDict(['SkillLevel']);
+  const skillLevelOptions = computed(() => getDictOptions('SkillLevel'));
+
+  const emit = defineEmits(['success']);
+  const visible = ref<boolean>(false);
+  const loading = ref<boolean>(false);
+  const formRef = ref();
+  const formData = reactive<Record<string, any>>({
+    id: '',
+    professionName: '',
+    workType: '',
+    skillLevel: '',
+    issueDate: null,
+    issueOrganization: '',
+    certificateNo: '',
+  });
+
+  function open(record?: Record<string, any>) {
+    formData.id = '';
+    formData.professionName = '';
+    formData.workType = '';
+    formData.skillLevel = '';
+    formData.issueDate = null;
+    formData.issueOrganization = '';
+    formData.certificateNo = '';
+    if (record) {
+      Object.assign(formData, record);
+      if (formData.issueDate && typeof formData.issueDate === 'string') formData.issueDate = dayjs(formData.issueDate);
+    }
+    visible.value = true;
+  }
+
+  async function handleOk() {
+    try {
+      await formRef.value?.validate();
+    } catch {
+      return;
+    }
+    loading.value = true;
+    try {
+      const result = { ...formData };
+      if (result.issueDate && dayjs.isDayjs(result.issueDate)) result.issueDate = result.issueDate.format('YYYY-MM-DD');
+      emit('success', result);
+      visible.value = false;
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  function handleCancel() {
+    visible.value = false;
+  }
+
+  defineExpose({ open });
+</script>

+ 110 - 0
jeecgboot-vue3/src/views/recruitment/personal/components/ResumeStudyAbroadModal.vue

@@ -0,0 +1,110 @@
+<template>
+  <j-modal :title="formData.id ? '编辑留学经历' : '新增留学经历'" :visible="visible" :width="700" centered @ok="handleOk" @cancel="handleCancel" :confirmLoading="loading">
+    <div style="padding: 20px;">
+      <a-form ref="formRef" :model="formData" :labelCol="{ span: 6 }" :wrapperCol="{ span: 16 }">
+        <a-row :gutter="24">
+          <a-col :span="12">
+            <a-form-item label="院校名称" name="schoolName" :rules="[{ required: true, message: '请输入院校名称' }]">
+              <a-input v-model:value="formData.schoolName" allow-clear placeholder="请输入院校名称"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="国家或地区" name="countryRegion" :rules="[{ required: true, message: '请输入国家或地区' }]">
+              <a-input v-model:value="formData.countryRegion" allow-clear placeholder="请输入国家或地区"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="留学专业" name="major" :rules="[{ required: true, message: '请输入留学专业' }]">
+              <a-input v-model:value="formData.major" allow-clear placeholder="请输入留学专业"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="是否毕业" name="isGraduated">
+              <a-radio-group v-model:value="formData.isGraduated">
+                <a-radio value="1">是</a-radio>
+                <a-radio value="0">否</a-radio>
+              </a-radio-group>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="开始时间" name="startDate" :rules="[{ required: true, message: '请选择开始时间' }]">
+              <a-date-picker v-model:value="formData.startDate" allow-clear placeholder="请选择开始时间" style="width: 100%" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="结束时间" name="endDate" :rules="[{ required: true, message: '请选择结束时间' }]">
+              <a-date-picker v-model:value="formData.endDate" allow-clear placeholder="请选择结束时间" style="width: 100%" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="海外学历认证书编号" name="diplomaCertNo">
+              <a-input v-model:value="formData.diplomaCertNo" allow-clear placeholder="请输入海外学历认证书编号"></a-input>
+            </a-form-item>
+          </a-col>
+        </a-row>
+      </a-form>
+    </div>
+  </j-modal>
+</template>
+
+<script lang="ts" setup>
+  import { defineExpose, reactive, ref } from 'vue';
+  import JModal from '/@/components/Modal/src/JModal/JModal.vue';
+  import dayjs from 'dayjs';
+
+  const emit = defineEmits(['success']);
+  const visible = ref<boolean>(false);
+  const loading = ref<boolean>(false);
+  const formRef = ref();
+  const formData = reactive<Record<string, any>>({
+    id: '',
+    schoolName: '',
+    countryRegion: '',
+    major: '',
+    isGraduated: '1',
+    startDate: null,
+    endDate: null,
+    diplomaCertNo: '',
+  });
+
+  function open(record?: Record<string, any>) {
+    formData.id = '';
+    formData.schoolName = '';
+    formData.countryRegion = '';
+    formData.major = '';
+    formData.isGraduated = '1';
+    formData.startDate = null;
+    formData.endDate = null;
+    formData.diplomaCertNo = '';
+    if (record) {
+      Object.assign(formData, record);
+      if (formData.startDate && typeof formData.startDate === 'string') formData.startDate = dayjs(formData.startDate);
+      if (formData.endDate && typeof formData.endDate === 'string') formData.endDate = dayjs(formData.endDate);
+    }
+    visible.value = true;
+  }
+
+  async function handleOk() {
+    try {
+      await formRef.value?.validate();
+    } catch {
+      return;
+    }
+    loading.value = true;
+    try {
+      const result = { ...formData };
+      if (result.startDate && dayjs.isDayjs(result.startDate)) result.startDate = result.startDate.format('YYYY-MM-DD');
+      if (result.endDate && dayjs.isDayjs(result.endDate)) result.endDate = result.endDate.format('YYYY-MM-DD');
+      emit('success', result);
+      visible.value = false;
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  function handleCancel() {
+    visible.value = false;
+  }
+
+  defineExpose({ open });
+</script>

+ 103 - 0
jeecgboot-vue3/src/views/recruitment/personal/components/ResumeTitleModal.vue

@@ -0,0 +1,103 @@
+<template>
+  <j-modal :title="formData.id ? '编辑职称情况' : '新增职称情况'" :visible="visible" :width="700" centered @ok="handleOk" @cancel="handleCancel" :confirmLoading="loading">
+    <div style="padding: 20px;">
+      <a-form ref="formRef" :model="formData" :labelCol="{ span: 6 }" :wrapperCol="{ span: 16 }">
+        <a-row :gutter="24">
+          <a-col :span="12">
+            <a-form-item label="职称名称" name="titleName" :rules="[{ required: true, message: '请输入职称名称' }]">
+              <a-input v-model:value="formData.titleName" allow-clear placeholder="请输入职称名称"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="专业" name="profession" :rules="[{ required: true, message: '请输入专业' }]">
+              <a-input v-model:value="formData.profession" allow-clear placeholder="请输入专业"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="级别" name="level" :rules="[{ required: true, message: '请选择级别' }]">
+              <a-select v-model:value="formData.level" :options="titleLevelOptions" allow-clear placeholder="请选择职称级别" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="发证日期" name="issueDate">
+              <a-date-picker v-model:value="formData.issueDate" allow-clear placeholder="请选择发证日期" style="width: 100%" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="发证单位" name="issueUnit">
+              <a-input v-model:value="formData.issueUnit" allow-clear placeholder="请输入发证单位"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="证书编号" name="certificateNo">
+              <a-input v-model:value="formData.certificateNo" allow-clear placeholder="请输入证书编号"></a-input>
+            </a-form-item>
+          </a-col>
+        </a-row>
+      </a-form>
+    </div>
+  </j-modal>
+</template>
+
+<script lang="ts" setup>
+  import { defineExpose, reactive, ref } from 'vue';
+  import JModal from '/@/components/Modal/src/JModal/JModal.vue';
+  import { useDict } from '/@/hooks/dictionary/useDict';
+  import { computed } from 'vue';
+  import dayjs from 'dayjs';
+
+  const { getDictOptions } = useDict(['TitleLevel']);
+  const titleLevelOptions = computed(() => getDictOptions('TitleLevel'));
+
+  const emit = defineEmits(['success']);
+  const visible = ref<boolean>(false);
+  const loading = ref<boolean>(false);
+  const formRef = ref();
+  const formData = reactive<Record<string, any>>({
+    id: '',
+    titleName: '',
+    profession: '',
+    level: '',
+    issueDate: null,
+    issueUnit: '',
+    certificateNo: '',
+  });
+
+  function open(record?: Record<string, any>) {
+    formData.id = '';
+    formData.titleName = '';
+    formData.profession = '';
+    formData.level = '';
+    formData.issueDate = null;
+    formData.issueUnit = '';
+    formData.certificateNo = '';
+    if (record) {
+      Object.assign(formData, record);
+      if (formData.issueDate && typeof formData.issueDate === 'string') formData.issueDate = dayjs(formData.issueDate);
+    }
+    visible.value = true;
+  }
+
+  async function handleOk() {
+    try {
+      await formRef.value?.validate();
+    } catch {
+      return;
+    }
+    loading.value = true;
+    try {
+      const result = { ...formData };
+      if (result.issueDate && dayjs.isDayjs(result.issueDate)) result.issueDate = result.issueDate.format('YYYY-MM-DD');
+      emit('success', result);
+      visible.value = false;
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  function handleCancel() {
+    visible.value = false;
+  }
+
+  defineExpose({ open });
+</script>

+ 93 - 0
jeecgboot-vue3/src/views/recruitment/personal/components/ResumeTrainingModal.vue

@@ -0,0 +1,93 @@
+<template>
+  <j-modal :title="formData.id ? '编辑培训经历' : '新增培训经历'" :visible="visible" :width="700" centered @ok="handleOk" @cancel="handleCancel" :confirmLoading="loading">
+    <div style="padding: 20px;">
+      <a-form ref="formRef" :model="formData" :labelCol="{ span: 6 }" :wrapperCol="{ span: 16 }">
+        <a-row :gutter="24">
+          <a-col :span="12">
+            <a-form-item label="培训机构" name="trainingInstitution" :rules="[{ required: true, message: '请输入培训机构' }]">
+              <a-input v-model:value="formData.trainingInstitution" allow-clear placeholder="请输入培训机构"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="培训课程" name="trainingCourse" :rules="[{ required: true, message: '请输入培训课程' }]">
+              <a-input v-model:value="formData.trainingCourse" allow-clear placeholder="请输入培训课程"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="开始时间" name="startDate" :rules="[{ required: true, message: '请选择开始时间' }]">
+              <a-date-picker v-model:value="formData.startDate" allow-clear placeholder="请选择开始时间" style="width: 100%" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="结束时间" name="endDate" :rules="[{ required: true, message: '请选择结束时间' }]">
+              <a-date-picker v-model:value="formData.endDate" allow-clear placeholder="请选择结束时间" style="width: 100%" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="资格证书号" name="certificateNo">
+              <a-input v-model:value="formData.certificateNo" allow-clear placeholder="请输入资格证书号"></a-input>
+            </a-form-item>
+          </a-col>
+        </a-row>
+      </a-form>
+    </div>
+  </j-modal>
+</template>
+
+<script lang="ts" setup>
+  import { defineExpose, reactive, ref } from 'vue';
+  import JModal from '/@/components/Modal/src/JModal/JModal.vue';
+  import dayjs from 'dayjs';
+
+  const emit = defineEmits(['success']);
+  const visible = ref<boolean>(false);
+  const loading = ref<boolean>(false);
+  const formRef = ref();
+  const formData = reactive<Record<string, any>>({
+    id: '',
+    trainingInstitution: '',
+    trainingCourse: '',
+    startDate: null,
+    endDate: null,
+    certificateNo: '',
+  });
+
+  function open(record?: Record<string, any>) {
+    formData.id = '';
+    formData.trainingInstitution = '';
+    formData.trainingCourse = '';
+    formData.startDate = null;
+    formData.endDate = null;
+    formData.certificateNo = '';
+    if (record) {
+      Object.assign(formData, record);
+      if (formData.startDate && typeof formData.startDate === 'string') formData.startDate = dayjs(formData.startDate);
+      if (formData.endDate && typeof formData.endDate === 'string') formData.endDate = dayjs(formData.endDate);
+    }
+    visible.value = true;
+  }
+
+  async function handleOk() {
+    try {
+      await formRef.value?.validate();
+    } catch {
+      return;
+    }
+    loading.value = true;
+    try {
+      const result = { ...formData };
+      if (result.startDate && dayjs.isDayjs(result.startDate)) result.startDate = result.startDate.format('YYYY-MM-DD');
+      if (result.endDate && dayjs.isDayjs(result.endDate)) result.endDate = result.endDate.format('YYYY-MM-DD');
+      emit('success', result);
+      visible.value = false;
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  function handleCancel() {
+    visible.value = false;
+  }
+
+  defineExpose({ open });
+</script>

+ 238 - 0
jeecgboot-vue3/src/views/recruitment/personal/components/ResumeWorkExpModal.vue

@@ -0,0 +1,238 @@
+<template>
+  <j-modal
+    :confirmLoading="loading"
+    :title="formData.id ? '编辑工作经历' : '新增工作经历'"
+    :visible="visible"
+    :width="700"
+    centered
+    @cancel="handleCancel"
+    @ok="handleOk"
+  >
+    <div style="padding: 20px">
+      <a-form ref="formRef" :labelCol="{ span: 6 }" :model="formData" :wrapperCol="{ span: 16 }">
+        <a-row :gutter="24">
+          <a-col :span="12">
+            <a-form-item :rules="[{ required: true, message: '请输入工作单位' }]" label="工作单位" name="companyName">
+              <a-input v-model:value="formData.companyName" allow-clear placeholder="请输入工作单位"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item :rules="[{ required: true, message: '请输入所在职位' }]" label="所在职位" name="position">
+              <a-input v-model:value="formData.position" allow-clear placeholder="请输入所在职位"></a-input>
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item :rules="[{ required: true, message: '请选择开始时间' }]" label="开始时间" name="startDate">
+              <a-date-picker v-model:value="formData.startDate" allow-clear placeholder="请选择开始时间" style="width: 100%" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="结束时间" name="endDate">
+              <a-date-picker v-model:value="formData.endDate" allow-clear placeholder="请选择结束时间" style="width: 100%" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="所属行业" name="industry">
+              <a-tree-select
+                v-model:value="industryValueObj"
+                :tree-data="industryTreeData"
+                placeholder="请选择所属行业"
+                allow-clear
+                label-in-value
+                @change="(val) => { formData.industry = val?.value || ''; }"
+              />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="单位类型" name="companyType">
+              <a-select v-model:value="formData.companyType" :options="companyTypeOptions" allow-clear placeholder="请选择单位类型" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="经济类型" name="economyType">
+              <a-tree-select
+                v-model:value="economyTypeValueObj"
+                :tree-data="economyTypeTreeData"
+                placeholder="请选择经济类型"
+                allow-clear
+                label-in-value
+                @change="(val) => { formData.economyType = val?.value || ''; }"
+              />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item label="人员规模" name="companySize">
+              <a-select v-model:value="formData.companySize" :options="companySizeOptions" allow-clear placeholder="请选择人员规模" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="24">
+            <a-form-item
+              :labelCol="{ span: 3 }"
+              :rules="[{ required: true, message: '请输入工作职责' }]"
+              :wrapperCol="{ span: 19 }"
+              label="工作职责"
+              name="workResponsibility"
+            >
+              <a-textarea v-model:value="formData.workResponsibility" :rows="2" allow-clear placeholder="请输入工作职责"></a-textarea>
+            </a-form-item>
+          </a-col>
+          <a-col :span="24">
+            <a-form-item
+              :labelCol="{ span: 3 }"
+              :rules="[{ required: true, message: '请输入工作业绩' }]"
+              :wrapperCol="{ span: 19 }"
+              label="工作业绩"
+              name="workAchievement"
+            >
+              <a-textarea v-model:value="formData.workAchievement" :rows="2" allow-clear placeholder="请输入工作业绩"></a-textarea>
+            </a-form-item>
+          </a-col>
+        </a-row>
+      </a-form>
+    </div>
+  </j-modal>
+</template>
+
+<script lang="ts" setup>
+import { computed, defineExpose, reactive, ref } from 'vue';
+import JModal from '/@/components/Modal/src/JModal/JModal.vue';
+import { useDict } from '/@/hooks/dictionary/useDict';
+import { defHttp } from '/@/utils/http/axios';
+import dayjs from 'dayjs';
+
+const { getDictOptions } = useDict(['CompanyType', 'CompanySize']);
+  const companyTypeOptions = computed(() => getDictOptions('CompanyType'));
+  const companySizeOptions = computed(() => getDictOptions('CompanySize'));
+
+  // ---------- 树形字典:所属行业 (EconomicIndustry) ----------
+  const industryTreeData = ref<any[]>([]);
+  const industryValueObj = ref<any>(undefined);
+
+  // ---------- 树形字典:经济类型 (EconomicType) ----------
+  const economyTypeTreeData = ref<any[]>([]);
+  const economyTypeValueObj = ref<any>(undefined);
+
+  /**
+   * 从 getDictTree 接口加载树形字典完整数据
+   * 后端返回格式: { label, value, children }
+   * 映射为 a-tree-select 识别的: { key, title, value, isLeaf, children }
+   * 注意:不能用 getDictTreeChildren 懒加载,因为这些字典的 Code 列为空,
+   * 导致 getDictTreeChildren 返回的 key 全是空字符串,所有节点 value 相同,
+   * 造成"选中一个全部选中"的问题。
+   * getDictTree 使用 Value 列作为唯一标识,可以正确区分各节点。
+   */
+  async function loadTreeData(dictionaryCode: string) {
+    const res = await defHttp.get({ url: '/system/dictionary/getDictTree', params: { dictionaryCode } }, { isTransformResponse: false });
+    if (res.success && res.result) {
+      return res.result.map((item) => treeNodeMapper(item));
+    }
+    return [];
+  }
+
+  /** 递归映射树节点:从后端 {label, value, children} 转为 {key, title, value, isLeaf, children} */
+  function treeNodeMapper(node: any): any {
+    return {
+      key: String(node.value),
+      title: node.label,
+      value: String(node.value),
+      isLeaf: !node.children || node.children.length === 0,
+      children: node.children?.length ? node.children.map((child) => treeNodeMapper(child)) : undefined,
+    };
+  }
+
+  /** 在树形字典数据中递归查找 value 对应的 label */
+  function findTreeNodeLabel(tree: any[], value: string): string | undefined {
+    for (const node of tree) {
+      if (node.value === value) return node.title;
+      if (node.children) {
+        const found = findTreeNodeLabel(node.children, value);
+        if (found) return found;
+      }
+    }
+    return undefined;
+  }
+
+  const emit = defineEmits(['success']);
+  const visible = ref<boolean>(false);
+  const loading = ref<boolean>(false);
+  const formRef = ref();
+  const formData = reactive<Record<string, any>>({
+    id: '',
+    companyName: '',
+    position: '',
+    startDate: null,
+    endDate: null,
+    industry: '',
+    companyType: '',
+    economyType: '',
+    companySize: '',
+    workResponsibility: '',
+    workAchievement: '',
+  });
+
+  async function open(record?: Record<string, any>) {
+    // 重置表单
+    formData.id = '';
+    formData.companyName = '';
+    formData.position = '';
+    formData.startDate = null;
+    formData.endDate = null;
+    formData.industry = '';
+    formData.companyType = '';
+    formData.economyType = '';
+    formData.companySize = '';
+    formData.workResponsibility = '';
+    formData.workAchievement = '';
+    // 重置树形字典状态
+    industryValueObj.value = undefined;
+    economyTypeValueObj.value = undefined;
+    industryTreeData.value = [];
+    economyTypeTreeData.value = [];
+
+    // 加载树形字典完整数据
+    industryTreeData.value = await loadTreeData('EconomicIndustry');
+    economyTypeTreeData.value = await loadTreeData('EconomicType');
+
+    if (record) {
+      Object.assign(formData, record);
+      // 日期字段转为 dayjs
+      if (formData.startDate && typeof formData.startDate === 'string') formData.startDate = dayjs(formData.startDate);
+      if (formData.endDate && typeof formData.endDate === 'string') formData.endDate = dayjs(formData.endDate);
+      // 从已加载的树数据中查找 label 用于编辑回显
+      if (record.industry) {
+        const label = findTreeNodeLabel(industryTreeData.value, record.industry) || record.industry;
+        industryValueObj.value = { value: record.industry, label };
+      }
+      if (record.economyType) {
+        const label = findTreeNodeLabel(economyTypeTreeData.value, record.economyType) || record.economyType;
+        economyTypeValueObj.value = { value: record.economyType, label };
+      }
+    }
+    visible.value = true;
+  }
+
+  async function handleOk() {
+    try {
+      await formRef.value?.validate();
+    } catch {
+      return;
+    }
+    loading.value = true;
+    try {
+      const result = { ...formData };
+      // 日期格式化
+      if (result.startDate && dayjs.isDayjs(result.startDate)) result.startDate = result.startDate.format('YYYY-MM-DD');
+      if (result.endDate && dayjs.isDayjs(result.endDate)) result.endDate = result.endDate.format('YYYY-MM-DD');
+      emit('success', result);
+      visible.value = false;
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  function handleCancel() {
+    visible.value = false;
+  }
+
+  defineExpose({ open });
+</script>