lifanagju_citu 5 mesi fa
parent
commit
6846e01c5c

+ 4 - 0
components.d.ts

@@ -35,6 +35,7 @@ declare module 'vue' {
     File: typeof import('./src/components/Upload/file.vue')['default']
     HeadSearch: typeof import('./src/components/headSearch/index.vue')['default']
     HotPromoted: typeof import('./src/components/Enterprise/hotPromoted.vue')['default']
+    IFrame: typeof import('./src/components/IFrame/index.vue')['default']
     Img: typeof import('./src/components/Upload/img.vue')['default']
     ImgCropper: typeof import('./src/components/ImgCropper/index.vue')['default']
     'Index copy': typeof import('./src/components/CtForm/index copy.vue')['default']
@@ -70,4 +71,7 @@ declare module 'vue' {
     VerifySlide: typeof import('./src/components/Verifition/Verify/VerifySlide.vue')['default']
     WangEditor: typeof import('./src/components/FormUI/wangEditor/index.vue')['default']
   }
+  export interface ComponentCustomProperties {
+    vLoading: typeof import('element-plus/es')['ElLoadingDirective']
+  }
 }

+ 17 - 0
src/api/recruit/personal/resume/index.js

@@ -1,5 +1,14 @@
 import request from '@/config/axios'
 
+// 简历解析-保存简历信息
+export const saveResumeInfo = async (data) => {
+  return await request.post({
+    url: '/app-api/menduner/system/person/resume/save',
+    openEncryption: true,
+    data
+  })
+}
+
 // 保存基本信息
 export const saveResumeBasicInfo = async (data) => {
   return await request.post({
@@ -226,6 +235,14 @@ export const resumeParser = async (params) => {
   })
 }
 
+// 附件简历解析2
+export const resumeParser2 = async (params) => {
+  return await request.get({
+    url: '/admin-api/menduner/system/online/resume/parser2',
+    params
+  })
+}
+
 // 修改求职类型
 export const updateJobStatus = async (data) => {
   return await request.post({

+ 1 - 1
src/components/CtDialog/index.vue

@@ -128,7 +128,7 @@ watch(() => props.visible, (newVal) => {
 })
 
 const dialogWidth = computed(() => {
-  const arr = ['900px', '1200px', '400px', '600px', '500px', '96%']
+  const arr = ['900px', '1200px', '400px', '600px', '500px', '96%', '90%']
   return arr[+props.widthType]
 })
 const handleClose = () => {

+ 42 - 0
src/components/IFrame/index.vue

@@ -0,0 +1,42 @@
+<script setup>
+import { ref, onMounted } from 'vue'
+defineOptions({ name: 'IFrame' })
+
+const props = defineProps({
+  initHeight: {
+    type: String,
+    default: ''
+  },
+  src: {
+    type: String,
+    default: ''
+  }
+})
+const loading = ref(true)
+const height = ref('')
+const frameRef = ref(null)
+const init = () => {
+  if (props.initHeight) {
+    height.value = props.initHeight
+  } else {
+    height.value = document.documentElement.clientHeight - 94.5 + 'px'
+  }
+  loading.value = false
+}
+onMounted(() => {
+  setTimeout(() => {
+    init()
+  }, 300)
+})
+</script>
+<template>
+  <div v-loading="loading" :style="'height:' + height">
+    <iframe
+      ref="frameRef"
+      :src="props.src"
+      frameborder="no"
+      scrolling="auto"
+      style="width: 100%; height: 100%"
+    ></iframe>
+  </div>
+</template>

+ 279 - 0
src/views/recruit/personal/PersonalCenter/resume/online/analysis/basicInfo.vue

@@ -0,0 +1,279 @@
+<template>
+  <CtForm ref="CtFormRef" :items="items" style="width: 100%;"></CtForm>
+</template>
+
+<script setup>
+defineOptions({ name: 'resumeAnalysis-basicInfo'})
+import { ref, watch } from 'vue'
+import { checkEmail } from '@/utils/validate'
+import { getDict } from '@/hooks/web/useDictionaries'
+const props = defineProps({
+  id: {
+    type: String,
+    default: ''
+  },
+  data: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+const CtFormRef = ref()
+const userInfo = ref(JSON.parse(localStorage.getItem('userInfo')))
+const items = ref({
+  options: [
+    {
+      type: 'text',
+      key: 'name',
+      value: null,
+      default: null,
+      label: '中文名 *',
+      col: 6,
+      outlined: true,
+      rules: [
+        value => {
+          if (value) return true
+          return '请输入您的中文名'
+        },
+        value => {
+          var regex = /^[\u4e00-\u9fa5]+$/
+          if (regex.test(value)) return true
+          return '请输入正确的中文名'
+        }
+      ]
+    },
+    {
+      type: 'text',
+      key: 'foreignName',
+      value: null,
+      label: '英文名',
+      col: 6,
+      outlined: true,
+      clearable: true,
+      rules: [
+        value => {
+          if (!value) return true
+          var regex = /^[A-Za-z]+(?:\s[A-Za-z]+)?$/
+          if (regex.test(value)) return true
+          return '请输入正确的英文名'
+        }
+      ]
+    },
+    {
+      type: 'ifRadio',
+      key: 'sex',
+      value: null, // '1' ? '男' : '女'
+      default: '1',
+      label: '性别 *',
+      col: 6,
+      width: 50,
+      dictTypeName: 'menduner_sex',
+      items: [],
+    },
+    {
+      type: 'phoneNumber',
+      key: 'phone',
+      value: null,
+      default: userInfo?.value?.phone || '',
+      label: '电话号码 *',
+      col: 6,
+      outlined: true,
+      rules: [v => !!v || '请填写联系手机号']
+    },
+    {
+      type: 'datePicker',
+      mode: 'date',
+      labelWidth: 110,
+      key: 'birthday',
+      value: '2000-01-01',
+      defaultValue: new Date(2000, 1, 1),
+      label: '出生日期 *',
+      disabledFutureDates: true,
+      col: 6,
+      format: 'YYYY/MM/DD',
+      flexStyle: 'mb-7',
+      outlined: true,
+      rules: [v => !!v || '请选择出生日期']
+    },
+    {
+      type: 'datePicker',
+      mode: 'month',
+      key: 'firstWorkTime',
+      value: '2000-01',
+      disabledFutureDates: true,
+      defaultValue: new Date(2000, 1),
+      format: 'YYYY/MM',
+      labelWidth: 110,
+      flexStyle: 'mb-7',
+      label: '首次工作时间',
+      col: 6,
+      outlined: true
+    },
+    {
+      type: 'text',
+      key: 'email',
+      value: null,
+      default: null,
+      label: '常用邮箱 *',
+      col: 6,
+      outlined: true,
+      rules: [
+        value => {
+          if (value) return true
+          return '请输入联系邮箱'
+        },
+        value => {
+          if (value && !checkEmail(value)) return '请输入正确的电子邮箱'
+          return true
+        }
+      ]
+    },
+    {
+      type: 'autocomplete',
+      key: 'expType',
+      value: null,
+      default: null,
+      label: '工作年限 *',
+      col: 6,
+      outlined: true,
+      itemText: 'label',
+      itemValue: 'value',
+      dictTypeName: 'menduner_exp_type',
+      rules: [v => !!v || '请选择工作年限'],
+      items: []
+    },
+    {
+      type: 'autocomplete',
+      key: 'eduType',
+      value: null,
+      default: null,
+      label: '最高学历 *',
+      col: 6,
+      outlined: true,
+      itemText: 'label',
+      itemValue: 'value',
+      dictTypeName: 'menduner_education_type',
+      rules: [v => !!v || '请选择最高学历'],
+      items: []
+    },
+    {
+      type: 'autocomplete',
+      key: 'jobType',
+      value: null,
+      default: null,
+      label: '求职类型 *',
+      col: 6,
+      outlined: true,
+      itemText: 'label',
+      itemValue: 'value',
+      dictTypeName: 'menduner_job_type',
+      rules: [v => !!v || '请选择求职类型'],
+      items: []
+    },
+    {
+      type: 'autocomplete',
+      key: 'jobStatus',
+      value: null,
+      default: null,
+      label: '求职状态 *',
+      col: 6,
+      outlined: true,
+      itemText: 'label',
+      itemValue: 'value',
+      dictTypeName: 'menduner_job_seek_status',
+      rules: [v => !!v || '请选择求职状态'],
+      items: []
+    },
+    {
+      type: 'autocomplete',
+      key: 'maritalStatus',
+      value: null,
+      default: null,
+      label: '婚姻状况',
+      col: 6,
+      outlined: true,
+      itemText: 'label',
+      itemValue: 'value',
+      dictTypeName: 'menduner_marital_status',
+      items: []
+    },
+    {
+      type: 'cascade',
+      key: 'areaId',
+      value: null,
+      default: null,
+      label: '所在城市',
+      itemText: 'name',
+      itemValue: 'id',
+      required: true,
+      checkStrictly: true,
+      clearable: false,
+      col: 6,
+      items: [],
+    },
+    {
+      type: 'cascade',
+      key: 'regId',
+      value: null,
+      default: null,
+      label: '户籍地',
+      itemText: 'name',
+      itemValue: 'id',
+      checkStrictly: true,
+      required: false,
+      clearable: true,
+      col: 6,
+      items: [],
+    },
+    
+  ]
+})
+
+items.value.options.forEach(async (e, index) => {
+  if ((index + 2) % 2 === 0) e.flexStyle = 'mr-3'
+  if (e.dictTypeName) {
+    const { data } = await getDict(e.dictTypeName)
+    e.items = data
+  }
+})
+
+watch(
+  () => props.data,
+  (newVal) => {
+    if (newVal && Object.keys(newVal)) {
+      items.value.options.forEach(e => {
+        if (newVal[e.key]) e.value = newVal[e.key]
+        else e.value = e.default || null
+      })
+    }
+  },
+  { immediate: true },
+)
+
+const submit = async () => {
+  const { valid } = await CtFormRef.value.formRef.validate()
+  if (!valid) return { id: props.id, data: null}
+  const obj = {}
+  items.value.options.forEach(e => obj[e.key] = e.value)
+  return { id: props.id, data: obj}
+}
+
+defineExpose({
+  id: props.id,
+  submit
+})
+
+getDict('areaTreeData', null, 'areaTreeData').then(({ data }) => {
+  data = data?.length && data || []
+  if (!data?.length) return console.error('areaTreeData获取失败!')
+  const chinaTreeData = data
+  if (!chinaTreeData?.length) return console.error('chinaTreeData获取失败!')
+  const workAreaProvince = items.value.options.find(e => e.key === 'areaId')
+  const regAreaProvince = items.value.options.find(e => e.key === 'regId')
+  if (workAreaProvince?.items) workAreaProvince.items = chinaTreeData
+  if (regAreaProvince?.items) regAreaProvince.items = chinaTreeData
+})
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 234 - 0
src/views/recruit/personal/PersonalCenter/resume/online/analysis/educationExp.vue

@@ -0,0 +1,234 @@
+<!--  -->
+<template>
+  <CtForm ref="CtFormRef" :items="items" style="width: 100%;"></CtForm>
+</template>
+
+<script setup>
+defineOptions({ name: 'resumeAnalysis-educationExp'})
+import { debounce } from 'lodash'
+import { getDict } from '@/hooks/web/useDictionaries'
+import { ref, reactive, watch } from 'vue'
+import { schoolSearchByName, schoolMajorByName } from '@/api/recruit/personal/resume'
+import { dealCanBeInputtedSave, dealCanBeInputtedValueAndLabel } from '@/utils/getText'
+
+const props = defineProps({
+  id: {
+    type: String,
+    default: ''
+  },
+  data: {
+    type: Object,
+    default: () => {}
+  }
+})
+
+const CtFormRef = ref()
+const dictItemsObj = reactive({})
+
+// 学校下拉列表
+const schoolNameInput = ref('')
+const getSchoolListData = async (name) => {
+  const item = items.value.options.find(e => e.key === 'schoolId')
+  if (!item) return
+  if (item.items?.length && (schoolNameInput.value === name)) return // 防抖
+  item[item.itemTextName] = schoolNameInput.value = name
+
+  if (name === null || name === '') { item.items = [] }
+  else {
+    const data = await schoolSearchByName({ name })
+    item.items = data
+  }
+}
+const debouncedCallbackSchool = debounce(newValue => {
+  if (!newValue) return
+  getSchoolListData(newValue)
+}, 500)
+
+// 专业下拉列表
+const majorNameInput = ref('')
+const getMajorListData = async (name) => {
+  const item = items.value.options.find(e => e.key === 'majorId')
+  if (name === '') { // 此接口不支持传空值
+    item.items = []
+    return
+  }
+  if (item.items?.length && (majorNameInput.value === name)) return // 防抖
+  item[item.itemTextName] = majorNameInput.value = name
+  
+  if (name === null || name === '') { item.items = [] }
+  else {
+    const data = await schoolMajorByName({ name })
+    item.items = data
+  }
+}
+const debouncedCallbackMajor = debounce(newValue => {
+  getMajorListData(newValue)
+}, 500)
+
+const items = ref({
+  options: [
+    {
+      type: 'combobox',
+      key: 'schoolId',
+      value: null,
+      default: null,
+      label: '学校名称 *',
+      col: 6,
+      outlined: true,
+      clearable: true,
+      canBeInputted: true, //
+      itemTextName: 'schoolName',
+      itemText: 'value',
+      itemValue: 'key',
+      rules: [v => !!v || '请选择学校名称'],
+      search: debouncedCallbackSchool,
+      items: []
+    },
+    {
+      type: 'combobox',
+      key: 'majorId',
+      value: null,
+      default: null,
+      label: '所学专业 *',
+      col: 6,
+      outlined: true,
+      clearable: true,
+      canBeInputted: true, //
+      itemTextName: 'major',
+      itemText: 'nameCn',
+      itemValue: 'id',
+      rules: [v => !!v || '请选择所学专业'],
+      search: debouncedCallbackMajor,
+      items: []
+    },
+    {
+      type: 'autocomplete',
+      key: 'educationType',
+      value: null,
+      default: null,
+      label: '学历 *',
+      col: 6,
+      outlined: true,
+      itemText: 'label',
+      itemValue: 'value',
+      rules: [v => !!v || '请选择学历'],
+      items: []
+    },
+    {
+      type: 'autocomplete',
+      key: 'educationSystemType',
+      value: null,
+      default: null,
+      label: '学制类型 *',
+      col: 6,
+      outlined: true,
+      itemText: 'label',
+      itemValue: 'value',
+      rules: [v => !!v || '请选择学制类型'],
+      items: dictItemsObj.educationSystemType,
+    },
+    {
+      type: 'datePicker',
+      key: 'startTime',
+      mode: 'month', // 时间类型 year month date time
+      value: null,
+      default: '2014-01',
+      format: 'YYYY/MM',
+      labelWidth: 120,
+      label: '开始时间 *',
+      defaultValue: new Date(2014, 1),
+      disabledFutureDates: true,
+      col: 6,
+      rules: [v => !!v || '请选择起始时间']
+    },
+    {
+      type: 'datePicker',
+      key: 'endTime',
+      mode: 'month', // 时间类型 year month date time
+      value: null,
+      default: '2018-01',
+      format: 'YYYY/MM',
+      defaultValue: new Date(2018, 1),
+      disabledFutureDates: true,
+      labelWidth: 120,
+      label: '结束时间 *',
+      col: 6,
+      rules: [v => !!v || '请选择结束时间']
+    },
+    {
+      type: 'textarea',
+      key: 'content',
+      value: null,
+      default: null,
+      rows: 5,
+      flexStyle: 'mt-5',
+      resize: true,
+      counter: 1600,
+      label: '在校经历',
+      outlined: true
+    },
+  ]
+})
+
+watch(
+  () => props.data,
+  (newVal) => {
+    if (newVal && Object.keys(newVal)) {
+      schoolNameInput.value = newVal.schoolName || null
+      majorNameInput.value = newVal.major || null
+      //
+      items.value.options.forEach(e => {
+        if (newVal[e.key]) e.value = newVal[e.key]
+        else e.value = e.default || null
+        if (e.canBeInputted) { // 特殊处理可输入下拉框
+          dealCanBeInputtedValueAndLabel(e, newVal)
+        }
+      })
+    }
+  },
+  { immediate: true },
+)
+
+const submit = async () => {
+  const { valid } = await CtFormRef.value.formRef.validate()
+  if (!valid) return { id: props.id, data: null}
+  const obj = {}
+  items.value.options.forEach(e => {
+    if (e.canBeInputted) { // 特殊处理可输入下拉框
+      dealCanBeInputtedSave(e, obj)
+    }
+    else obj[e.key] = e.value
+  })
+  return { id: props.id, data: obj}
+}
+
+defineExpose({
+  id: props.id,
+  submit
+})
+
+// 左侧加mr
+items.value.options.forEach((e, index) => {
+  if (((index + 2) % 2 === 0) && Boolean(e.col) && e.col !== 12) e.flexStyle = 'mr-3'
+})
+
+// 获取字典内容
+const dictList = [
+  { type: 'menduner_education_type', key: 'educationType' },
+  { type: 'menduner_education_system_type', key: 'educationSystemType' }
+]
+const getDictData = async (obj) => {
+  const item = items.value.options.find(e => e.key === obj.key)
+  if (item) { //  && !item.items?.length
+    const { data } = await getDict(obj.type)
+    item.items = data || []
+    dictItemsObj[obj.key] = data || []
+  }
+}
+const getOptions = () => {
+  dictList.forEach(obj =>  getDictData(obj))
+}
+getOptions()
+</script>
+<style lang="scss" scoped>
+</style>

+ 59 - 0
src/views/recruit/personal/PersonalCenter/resume/online/analysis/selfEvaluation.vue

@@ -0,0 +1,59 @@
+<!--  -->
+<template>
+  <div>
+    <v-textarea 
+      v-model="advantage" 
+      :label="$t('resume.dataDefaultPrompt') + $t('resume.personalAdvantages') + '...'" 
+      variant="outlined" 
+      counter="300" 
+      color="primary" 
+      validate-on="input"
+      :rules="advantageRules"
+    ></v-textarea>
+  </div>
+</template>
+
+<script setup>
+defineOptions({ name: 'resumeAnalysis-selfEvaluation'})
+// import Snackbar from '@/plugins/snackbar'
+import DOMPurify from 'dompurify'
+import { ref } from 'vue'
+const props = defineProps({
+  id: {
+    type: String,
+    default: ''
+  },
+  data: {
+    type: String,
+    default: ''
+  }
+})
+
+const advantage = ref(props.data)
+const advantageRules = ref([
+  value => {
+    if (value) return true
+    return '请输入您的个人优势'
+  },
+  value => {
+    if (value?.length <= 300) return true
+    return '请输入2-300个字符'
+  }
+])
+
+const submit = async () => {
+  advantage.value = DOMPurify.sanitize(advantage.value)
+  if (!advantage.value) {
+    // Snackbar.warning('请先输入个人优势!')
+    return { id: props.id, data: null}
+  }
+  return { id: props.id, data: { content: advantage.value}}
+}
+
+defineExpose({
+  id: props.id,
+  submit
+})
+</script>
+<style lang="scss" scoped>
+</style>

+ 112 - 0
src/views/recruit/personal/PersonalCenter/resume/online/analysis/trainingExperience.vue

@@ -0,0 +1,112 @@
+<!--  -->
+<template>
+  <CtForm ref="CtFormRef" :items="items" style="width: 100%;"></CtForm>
+</template>
+
+<script setup>
+defineOptions({ name: 'resumeAnalysis-trainingExperience'})
+import { ref, watch } from 'vue'
+const props = defineProps({
+  id: {
+    type: String,
+    default: ''
+  },
+  data: {
+    type: Object,
+    default: () => {}
+  }
+})
+
+const CtFormRef = ref()
+const items = ref({
+  options: [
+    {
+      type: 'text',
+      key: 'orgName',
+      value: '',
+      col: 6,
+      label: '培训中心 *',
+      flexStyle: 'mr-3',
+      rules: [v => !!v || '请输入培训中心']
+    },
+    {
+      type: 'text',
+      key: 'course',
+      value: '',
+      col: 6,
+      label: '培训课程 *',
+      rules: [v => !!v || '请输入培训课程']
+    },
+    {
+      type: 'datePicker',
+      key: 'startTime',
+      mode: 'month',
+      value: '2020-01',
+      labelWidth: 140,
+      format: 'YYYY/MM',
+      defaultValue: new Date(2020, 1),
+      label: '培训开始时间 *',
+      disabledFutureDates: true,
+      col: 6,
+      flexStyle: 'mr-3',
+      rules: [v => !!v || '请选择培训开始时间']
+    },
+    {
+      type: 'datePicker',
+      key: 'endTime',
+      mode: 'month',
+      value: '2022-01',
+      format: 'YYYY/MM',
+      labelWidth: 140,
+      defaultValue: new Date(2022, 1),
+      label: '培训结束时间 *',
+      disabledFutureDates: true,
+      col: 6,
+      rules: [v => !!v || '请选择培训结束时间']
+    },
+    {
+      type: 'textarea',
+      key: 'content',
+      value: '',
+      label: '培训描述',
+      rows: 5,
+      flexStyle: 'mt-5',
+      resize: true,
+      counter: 2000,
+      rules: [
+        value => {
+          if (value?.length <= 2000) return true
+          return '请输入2-2000个字符'
+        }
+      ]
+    }
+  ]
+})
+
+watch(
+  () => props.data,
+  (newVal) => {
+    if (newVal && Object.keys(newVal)) {
+      items.value.options.forEach(e => {
+        if (newVal[e.key]) e.value = newVal[e.key]
+      })
+    }
+  },
+  { immediate: true }
+)
+
+const submit = async () => {
+  const { valid } = await CtFormRef.value.formRef.validate()
+  if (!valid) return { id: props.id, data: null}
+  const obj = {}
+  items.value.options.forEach(e => obj[e.key] = e.value)
+  return { id: props.id, data: obj}
+}
+
+defineExpose({
+  id: props.id,
+  submit
+})
+</script>
+<style lang="scss" scoped>
+</style>

+ 203 - 0
src/views/recruit/personal/PersonalCenter/resume/online/analysis/workExperience.vue

@@ -0,0 +1,203 @@
+<!--  -->
+<template>
+  <CtForm ref="CtFormRef" :items="items" style="width: 100%;">
+    <template #endTime="{ item }">
+      <div>
+        <v-checkbox-btn
+          v-model="item.soFar"
+          color="primary"
+          :label="$t('sys.soFar')"
+          class="ml-2"
+          :disabled="false"
+          :style="`line-height: ${item.dense === 'default' ? 56 : item.dense === 'comfortable' ? 48 : 40 }px;`"
+          style="width: 80px;"
+          hide-details
+          @update:modelValue="v => handleSoFarChange(v, item)"
+        ></v-checkbox-btn>
+      </div>
+    </template>
+  </CtForm>
+</template>
+
+<script setup>
+defineOptions({ name: 'resumeAnalysis-workExperience'})
+import { getDict } from '@/hooks/web/useDictionaries'
+import { ref, reactive, watch } from 'vue'
+import { dealCanBeInputtedSave, dealCanBeInputtedValueAndLabel } from '@/utils/getText'
+
+const props = defineProps({
+  id: {
+    type: String,
+    default: ''
+  },
+  data: {
+    type: Object,
+    default: () => {}
+  }
+})
+
+const CtFormRef = ref()
+const dictItemsObj = reactive({})
+dictItemsObj.educationSystemType = [{ label: '全日制', value: '0' }, { label: '非全日制', value: '1' }]
+
+
+// 企业名称下拉列表
+const enterpriseNameInput = ref('')
+const getEnterpriseData = async (name) => {
+  const item = items.value.options.find(e => e.key === 'enterpriseId')
+  if (!item) return
+  if (item.items?.length && (enterpriseNameInput.value === name)) return // 防抖
+  item[item.itemTextName] = enterpriseNameInput.value = name
+  if (name === null || name === '') { item.items = [] }
+  else {
+    const data = await enterpriseSearchByName({ name })
+    item.items = data
+  }
+}
+
+const positionSearch = (name) => {
+  const item = items.value.options.find(e => e.key === 'positionId')
+  if (!item) return
+  item[item.itemTextName] = name
+}
+
+// 《至今》复选框事件
+const handleSoFarChange = (bool, item) => {
+  const opObj = items.value.options.find(e => e.key === item.key)
+  if (opObj) {
+    opObj.value = null
+    opObj.soFar = bool
+    opObj.disabled = bool ? true : false
+  }
+}
+
+const items = ref({
+  options: [
+    {
+      type: 'combobox',
+      key: 'enterpriseId',
+      value: null,
+      default: null,
+      label: '企业名称 *',
+      outlined: true,
+      clearable: true,
+      canBeInputted: true, //
+      itemTextName: 'enterpriseName',
+      itemText: 'value',
+      itemValue: 'key',
+      rules: [v => !!v || '请选择企业名称'],
+      search: getEnterpriseData,
+      items: []
+    },
+    {
+      type: 'combobox',
+      key: 'positionId',
+      value: null,
+      default: null,
+      label: '职位名称 *',
+      outlined: true,
+      clearable: true,
+      canBeInputted: true, //
+      itemTextName: 'positionName',
+      itemText: 'nameCn',
+      itemValue: 'id',
+      rules: [v => !!v || '请选择职位名称'],
+      search: val => positionSearch(val),
+      items: []
+    },
+    {
+      type: 'datePicker',
+      key: 'startTime',
+      mode: 'month', // 时间类型 year month date time
+      value: null,
+      label: '开始时间 *',
+      labelWidth: 100,
+      default: '2020-01',
+      defaultValue: new Date(2020, 1),
+      disabledFutureDates: true,
+      format: 'YYYY/MM',
+      rules: [v => !!v || '请选择起始时间']
+    },
+    {
+      type: 'datePicker',
+      key: 'endTime',
+      slotName: 'endTime',
+      mode: 'month', // 时间类型 year month date time
+      value: null,
+      format: 'YYYY/MM',
+      default: '2022-01',
+      defaultValue: new Date(2022, 1),
+      label: '结束时间 *',
+      disabledFutureDates: true,
+      labelWidth: 100,
+      rules: [v => !!v || '请选择结束时间']
+    },
+    {
+      type: 'textarea',
+      key: 'content',
+      value: null,
+      default: null,
+      rows: 10,
+      resize: true,
+      flexStyle: 'mt-5',
+      counter: 2000,
+      label: '工作内容 *',
+      outlined: true,
+      rules: [v => !!v || '请输入工作内容']
+    },
+  ]
+})
+// 左侧加mr
+// items.value.options.forEach((e, index) => {
+//   if (((index + 2) % 2 === 0) && Boolean(e.col) && e.col !== 12) e.flexStyle = 'mr-3'
+// })
+
+watch(
+  () => props.data,
+  (newVal) => {
+    if (newVal && Object.keys(newVal)) {
+      enterpriseNameInput.value = newVal.enterpriseName
+      //
+      items.value.options.forEach(e => {
+        if (newVal[e.key]) e.value = newVal[e.key]
+        if (e.canBeInputted) { // 特殊处理可输入下拉框
+          dealCanBeInputtedValueAndLabel(e, newVal)
+        }
+      })
+      if (!newVal.endTime) handleSoFarChange(true, {key: 'endTime'})
+    }
+  },
+  { immediate: true },
+)
+
+const submit = async () => {
+  const { valid } = await CtFormRef.value.formRef.validate()
+  if (!valid) return { id: props.id, data: null}
+  const obj = {}
+  items.value.options.forEach(e => {
+    if (e.canBeInputted) { // 特殊处理可输入下拉框
+      dealCanBeInputtedSave(e, obj)
+    }
+    else obj[e.key] = e.value
+  })
+  return { id: props.id, data: obj}
+}
+
+defineExpose({
+  id: props.id,
+  submit
+})
+
+// 岗位treeChildren
+let positionTreeChildrenData = []
+getDict('positionTreeData', null, 'positionTreeData').then(({ data }) => {
+  data = data?.length && data || []
+  data.forEach(e => {
+    if (e?.children?.length) positionTreeChildrenData = positionTreeChildrenData.concat(e.children)
+  })
+  items.value.options.find(e => e.key === 'positionId').items = positionTreeChildrenData
+})
+
+</script>
+<style lang="scss" scoped>
+</style>

+ 1 - 1
src/views/recruit/personal/PersonalCenter/resume/online/components/educationExp.vue

@@ -85,11 +85,11 @@ const debouncedCallbackSchool = debounce(newValue => {
 // 专业下拉列表
 const majorNameInput = ref('')
 const getMajorListData = async (name) => {
+  const item = formItems.value.options.find(e => e.key === 'majorId')
   if (name === '') { // 此接口不支持传空值
     item.items = []
     return
   }
-  const item = formItems.value.options.find(e => e.key === 'majorId')
   if (item.items?.length && (majorNameInput.value === name)) return // 防抖
   item[item.itemTextName] = majorNameInput.value = name
   

+ 15 - 13
src/views/recruit/personal/PersonalCenter/resume/online/index.vue

@@ -3,7 +3,7 @@
     <div class="tabHeader">
       <div class="d-flex align-center justify-space-between">
         <ProgressBar :num="completeNum" :total="items?.length" style="width: 100%;"></ProgressBar>
-        <!-- <v-btn variant="text" color="primary" @click="handleImportAttachment">导入已有简历</v-btn> -->
+        <v-btn variant="text" color="primary" @click="handleImportAttachment">导入已有简历</v-btn>
       </div>
       <v-tabs v-model="tab" align-tabs="start" color="primary" bg-color="#f7f8fa">
         <v-tab v-for="k in items" :key="k.path" :value="k.value" @click="handleClick(k)">
@@ -29,7 +29,7 @@
 
   <Loading :visible="loading"></Loading>
   <selectResumeDialog v-model="showAttachment" title="请选择已有的简历导入" :list="attachmentList" @submit="handleAttachmentSubmit" @close="showAttachment = false" />
-  <resumeAnalysis v-model="showAnalysis" :data="result" :url="selectAttachment" @submit="handleResumeAnalysisSubmit" @close="showAnalysis = false" />
+  <resumeAnalysis v-model="showAnalysis" :data="result" :fileUrl="encodeURIComponent(fileUrl)" @close="showAnalysis = false" />
 </template>
 
 <script setup>
@@ -44,11 +44,13 @@ import educationExp from './components/educationExp.vue'
 import workExperience from './components/workExperience.vue'
 // import projectExperience from './components/projectExperience.vue'
 import vocationalSkills from './components/vocationalSkills.vue'
-import { resumePersonFillAll, getPersonResumeCv, resumeParser } from '@/api/recruit/personal/resume'
+import { resumePersonFillAll, getPersonResumeCv, resumeParser2 } from '@/api/recruit/personal/resume'
 import Snackbar from '@/plugins/snackbar'
 import { useRouter } from 'vue-router'
 import selectResumeDialog from '@/views/recruit/personal/position/components/jobDetails/selectResumeDialog.vue'
 import resumeAnalysis from './resumeAnalysis.vue'
+import { Base64 } from 'js-base64'
+// import { dataObj } from './test.js'
 
 const router = useRouter()
 const { t } = useI18n()
@@ -114,7 +116,7 @@ const getAttachmentList = async () => {
   const data = await getPersonResumeCv()
   attachmentList.value = data
 }
-// getAttachmentList()
+getAttachmentList()
 
 // 导入附件简历
 const showAttachment = ref(false)
@@ -124,32 +126,32 @@ const handleImportAttachment = () => {
     router.push('/recruit/personal/personalCenter/resume/attachment')
     return
   }
+  showAnalysis.value = false
   showAttachment.value = true
 }
 
+// const result = ref(JSON.parse(JSON.stringify(dataObj)))
 const result = ref({})
-// const fileUrl = 'https://minio.menduner.com/test/person/1/attachment/7cde29dc69c1403649be55d4c2bfd3d8304c088dc79ab25afe9c4bf55d3b382f.docx'
-// const fileUrl = 'https://minio.menduner.com/dev/person/546458957531713536/attachment/a71eeef077f41bb9edc9eae904f6cc8337c01431435c319612c7ce5d9258a808.pdf'
-const fileUrl = 'https://minio.menduner.com/dev/person/744285421114101760/attachment/8908069ceead39d55852f126c0a46dc5f338c1c9aae5c8e7b6838cddf2d57deb.pdf'
-const selectAttachment = ref('')
+const fileUrl = ref('')
 const showAnalysis = ref(false)
 const handleAttachmentSubmit = async (val) => {
   if (!val) return
-  selectAttachment.value = attachmentList.value.find(e => e.id === val).url
+  const url = attachmentList.value.find(e => e.id === val).url
+  const baseUrl = import.meta.env.VITE_PREVIEW_URL
+  fileUrl.value = !url.includes('.pdf') ?  `${baseUrl}/onlinePreview?url=${encodeURIComponent(Base64.encode(url))}` : url
   showAttachment.value = false
   loading.value = true
   try {
-    const data = await resumeParser({ fileUrl })
-    result.value = data.result
+    const data = await resumeParser2({ fileUrl: fileUrl.value })
+    result.value = data || {}
+    showAnalysis.value = true
   } catch (error) {
     console.log(error)
   } finally {
     loading.value = false
-    showAnalysis.value = true
   }
 }
 
-const handleResumeAnalysisSubmit = () => {}
 </script>
 
 <style scoped lang="scss">

+ 159 - 11
src/views/recruit/personal/PersonalCenter/resume/online/resumeAnalysis.vue

@@ -2,38 +2,186 @@
 <template>
   <CtDialog
     :visible="show"
-    :widthType="0"
-    :footer="false"
+    :widthType="6"
+    :footer="true"
     titleClass="text-h6"
     title="附件简历解析内容"
     @close="emit('close')"
+    @submit="submit"
   >
-    <Evaluation v-if="data.contMyDesc" :data="data" />
-    <WorkExp v-if="data.jobExpObjs && data.jobExpObjs.length" :data="data" />
-    <EduExp v-if="data.educationObjs && data.educationObjs.length" :data="data" />
+    <div class="d-flex" style="height: calc(90vh - 160px); overflow: hidden; position: relative;">
+      <!-- 简历附件回显 -->
+      <div class="mr-5" style="width: 60%; height: 100%">
+        <IFrame :src="decodeURIComponent(props.fileUrl)" initHeight="100%"></IFrame>
+      </div>
+      <!-- 解析内容 -->
+      <v-card class="elevation-2 pa-3" style="flex: 1; height: 100%; overflow: auto;" >
+        <div v-for="item, index of paths" :key="item.id" :id="item.id" class="pa-3 mb-3" style="background-color: var(--color-f8);">
+          <div class="resume-header mb-3">
+            <div class="resume-title">{{ item.text }}</div>
+            <v-btn variant="text" color="error" density="compact" prepend-icon="mdi-delete-outline" @click="del(index, item)">删除</v-btn>
+          </div>
+          <component ref="componentRef" :id="item.id" :is="item.path" :data="item.data" />
+        </div>
+      </v-card>
+      <v-overlay
+        v-model="loading"
+        :close-on-content-click="false"
+        :no-click-animation="true"
+        contained
+        class="align-center justify-center"
+      >
+        <v-progress-circular color="primary" size="64" indeterminate></v-progress-circular>
+      </v-overlay>
+    </div>
   </CtDialog>
 </template>
 
 <script setup>
 defineOptions({name: 'position-details-selectResumeDialog'})
-import { watch, computed } from 'vue'
-import WorkExp from './analysis/workExp.vue'
-import EduExp from './analysis/eduExp.vue'
-import Evaluation from './analysis/evaluation.vue'
-
+import { watch, computed, shallowRef, ref } from 'vue'
+import { useI18n } from '@/hooks/web/useI18n'
+import IFrame from '@/components/IFrame'
+import basicInfo from './analysis/basicInfo.vue'
+import selfEvaluation from './analysis/selfEvaluation.vue'
+import educationExp from './analysis/educationExp.vue'
+import workExperience from './analysis/workExperience.vue'
+import trainingExperience from './analysis/trainingExperience.vue'
+import Snackbar from '@/plugins/snackbar'
+import Confirm from '@/plugins/confirm'
+import { saveResumeInfo } from '@/api/recruit/personal/resume'
+import { useUserStore } from '@/store/user'; const userStore = useUserStore()
 const emit = defineEmits(['update:modelValue', 'close'])
+const { t } = useI18n()
 const props = defineProps({
   modelValue: [Boolean, Number],
+  fileUrl: {
+    type: String,
+    default: ''
+  },
   data: {
     type: Object,
-    default: () => ({})
+    default: () => {}
   }
 })
 
+const exampleList = {
+  person: { text: t('resume.basicInfo'), id: 'person', path: basicInfo, hide: false },
+  advantage: { text: t('resume.personalAdvantages'), id: 'advantage', path: selfEvaluation, hide: false },
+  eduList: { text: t('resume.educationExp'), id: 'eduList', path: educationExp, hide: false },
+  workList: { text: t('resume.workExperience'), id: 'workList', path: workExperience, hide: false },
+  trainList: { text: t('resume.trainingExperience'), id: 'trainList', path: trainingExperience, hide: false },
+}
+
+const componentRef = ref()
+const getValue = async () => {
+  let id = ''
+  let data = {}
+  for (let index = 0; index < componentRef.value.length; index++) {
+    const e = componentRef.value[index]
+    const query = await e.submit()
+    if (query && query.data) {
+      data[query.id] = query.data
+    } else {
+      id = id ? id : query.id
+    }
+  }
+  console.log('id:', id)
+  if (id) {
+    Snackbar.warning('请填写完整后提交!')
+    return
+  }
+  // 处理data
+  const obj = Object.keys(data).length ? {} : null
+  const keyTransform = { // 转换给后端的key
+    eduList: 'eduExp',
+    workList: 'workExp',
+    trainList: 'trainExp',
+  }
+  if (obj) {
+    Object.keys(data).forEach(key => {
+      if (key.includes('_')) { // 数组
+        const oldKey = key.split('_')[0]
+        const newKey = keyTransform[oldKey] ? keyTransform[oldKey] : oldKey
+        if (!obj[newKey]) obj[newKey] = [data[key]]
+        else obj[newKey].push(data[key])
+      } else {
+        const newKey = keyTransform[key] ? keyTransform[key] : key
+        obj[newKey] = data[key]
+      }
+    })
+    obj.avatar = ""
+    obj.tag = { tagList: [] }
+    obj.jobInterested = []
+  }
+  // console.log('123456:', obj)
+  return obj && Object.keys(obj).length ? JSON.stringify(obj) : null
+}
+const loading = ref(false)
+const submit = async () => {
+  const obj = await getValue()
+  if (!obj) return
+  try {
+    loading.value = true
+    await saveResumeInfo(obj)
+    Snackbar.success(t('common.saveMsg'))
+    emit('close')
+    await useUserStore().getUserBaseInfos() // 更新用户信息
+    setTimeout(() => { window.location.reload() }, 1000)
+  } catch (error) {
+    console.log(error)
+  } finally {
+    loading.value = false
+  }
+}
+
+const del = (index, item) => {
+  Confirm('系统提示', `是否确认删除${item.text}?`).then(async () => {
+    paths.value = paths.value.filter(e => e.id !== item.id)
+  })
+}
+
 const show = computed(() => {
   return props.modelValue
 })
 
+const paths = shallowRef([])
+watch(
+  () => props.data,
+  (newVal) => {
+    paths.value = []
+    if (newVal && Object.keys(newVal)) {
+      if (newVal.person?.advantage) newVal.advantage = newVal.person.advantage
+      // obj
+      const dealObjKeys = ['person', 'advantage']
+      dealObjKeys.forEach(key => {
+        if (newVal[key]) {
+          const obj = {...exampleList[key]}
+          obj.data = newVal[key]
+          paths.value.push(obj)
+        }
+      })
+      // const baseInfoItem = { ...exampleList.person }
+      // if (newVal.person) baseInfoItem = newVal.person; paths.value.push(baseInfoItem)
+      // if (newVal.person?.advantage) exampleList.advantage.data = newVal.person.advantage; paths.value.push(exampleList.advantage)
+      // arr
+      const dealArrKeys = ['eduList', 'workList', 'trainList']
+      dealArrKeys.forEach(key => {
+        if (newVal[key]?.length) {
+          for (let index = 0; index < newVal[key].length; index++) {
+            const obj = {...exampleList[key]}
+            obj.id = obj.id + '_' + index
+            obj.text = newVal[key].length > 1 ? obj.text + (index+1) : obj.text
+            obj.data = newVal[key][index]
+            paths.value.push(obj)
+          }
+        }
+      })
+    }
+  },
+  { immediate: true },
+)
+
 watch(() => show.value, (newVal) => {
   emit('update:modelValue', newVal)
 })

File diff suppressed because it is too large
+ 9 - 0
src/views/recruit/personal/PersonalCenter/resume/online/test.js


Some files were not shown because too many files changed in this diff