infoForm.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. <template>
  2. <div style="width: 100%;">
  3. <CtForm ref="formPageRef" :items="items">
  4. <!-- 头像 -->
  5. <template #avatar="{ item }">
  6. <!-- <div class="d-flex flex-column">
  7. <div class="d-flex align-center">
  8. </div>
  9. </div> -->
  10. <div style="color: #7a7a7a; min-width: 52px;">头像:</div>
  11. <div class="avatarsBox" @mouseover="showIcon = true" @mouseleave="showIcon = false">
  12. <v-avatar class="elevation-5" size=80 :image="getUserAvatar(item.value, male)"></v-avatar>
  13. <div v-show="showIcon" @click="openFileInput" v-bind="$attrs" class="mdi mdi-camera-outline">
  14. <input
  15. type="file"
  16. ref="fileInput"
  17. accept="image/png, image/jpg, image/jpeg"
  18. style="display: none;"
  19. @change="handleUploadFile"
  20. />
  21. </div>
  22. </div>
  23. <div style="font-size: 14px; color: var(--color-999);">只支持JPG、JPEG、PNG类型的图片,大小不超过20M</div>
  24. </template>
  25. <template #analysis>
  26. <div style="text-align: right; width: 100%;">
  27. <v-btn variant="text" color="primary" :loading="analyzeLoading" @click="handleImportAttachment">导入简历快速填写 ({{ attachmentCount }}/5)</v-btn>
  28. </div>
  29. </template>
  30. </CtForm>
  31. </div>
  32. <ImgCropper :visible="isShowCopper" :image="selectPic" :cropBoxResizable="true" @submit="handleHideCopper" :aspectRatio="1 / 1" @close="isShowCopper = false"></ImgCropper>
  33. <!-- 选择本地简历 -->
  34. <CtDialog
  35. :visible="openUploadDialog"
  36. :widthType="2"
  37. titleClass="text-h6"
  38. @close="openUploadDialog = false"
  39. title="附件简历上传"
  40. @submit="uploadFileSubmit"
  41. >
  42. <uploadForm ref="uploadFormRef"></uploadForm>
  43. <div class="color-warning" style="font-size: 14px;">提示:上传成功后将自动解析,导入解析简历尝试不得超过5次,请确认好简历后上传!</div>
  44. </CtDialog>
  45. <Loading :visible="analyzeLoading"></Loading>
  46. </template>
  47. <script setup>
  48. import { getDict } from '@/hooks/web/useDictionaries'
  49. defineOptions({name: 'dialogExtend-InfoForm'})
  50. import { reactive, ref } from 'vue'
  51. import { checkEmail } from '@/utils/validate'
  52. import {
  53. resumeParser2,
  54. getPersonResumeCv,
  55. enterpriseSearchByName,
  56. savePersonResumeCv
  57. } from '@/api/recruit/personal/resume'
  58. import { getUserAvatar } from '@/utils/avatar'
  59. import { uploadFile } from '@/api/common'
  60. import Snackbar from '@/plugins/snackbar'
  61. import { useI18n } from '@/hooks/web/useI18n'; const { t } = useI18n()
  62. import uploadForm from './upload.vue'
  63. import { analyzeTestData } from './analyzeTestData.js'
  64. const props = defineProps({
  65. option: {
  66. type: Object,
  67. default: () => {}
  68. }
  69. })
  70. const setInfo = ref(props.option?.setInfo ? props.option.setInfo : {})
  71. const formPageRef = ref()
  72. let query = reactive({})
  73. // 企业名称下拉列表
  74. let enterpriseName = null
  75. // const enterpriseNameInput = ref('')
  76. const getEnterpriseData = async (name) => {
  77. const item = items.value.options.find(e => e.key === 'enterpriseId')
  78. if (!item) return
  79. if (item.items?.length && (enterpriseName === name)) return // 防抖
  80. // item[item.itemTextName] =
  81. enterpriseName = name
  82. if (name === null || name === '') { item.items = [] }
  83. else {
  84. const data = await enterpriseSearchByName({ name })
  85. item.items = data
  86. }
  87. }
  88. // 图片裁剪
  89. const selectPic = ref('')
  90. const isShowCopper = ref(false)
  91. const male = ref('1')
  92. const showIcon = ref(false)
  93. let positionName = null
  94. const items = ref({
  95. options: [
  96. {
  97. slotName: 'avatar',
  98. key: 'avatar',
  99. value: '',
  100. flexStyle: 'align-center mb-3'
  101. },
  102. {
  103. slotName: 'analysis',
  104. },
  105. {
  106. type: 'text',
  107. key: 'name',
  108. value: '',
  109. default: null,
  110. label: '姓名 *',
  111. outlined: true,
  112. rules: [
  113. value => {
  114. if (value) return true
  115. return '请输入您的中文名'
  116. },
  117. value => {
  118. var regex = /^[\u4e00-\u9fa5]+$/
  119. if (regex.test(value)) return true
  120. return '请输入正确的中文名'
  121. }
  122. ]
  123. },
  124. {
  125. type: 'autocomplete',
  126. key: 'sex',
  127. value: '1', // '1' ? '男' : '女'
  128. default: '1',
  129. label: '性别 *',
  130. outlined: true,
  131. dictTypeName: 'menduner_sex',
  132. rules: [v => !!v || '请选择性别'],
  133. items: [],
  134. change: val => male.value = val
  135. },
  136. {
  137. type: 'phoneNumber',
  138. key: 'phone',
  139. value: '',
  140. clearable: true,
  141. label: '联系手机号 *',
  142. rules: [v => !!v || '请填写联系手机号']
  143. },
  144. {
  145. type: 'text',
  146. key: 'email',
  147. value: null,
  148. default: null,
  149. label: '常用邮箱 *',
  150. outlined: true,
  151. rules: [
  152. value => {
  153. if (value) return true
  154. return '请输入联系邮箱'
  155. },
  156. value => {
  157. if (value && !checkEmail(value)) return '请输入正确的电子邮箱'
  158. return true
  159. }
  160. ]
  161. },
  162. {
  163. type: 'datePicker',
  164. mode: 'date',
  165. labelWidth: 80,
  166. key: 'birthday',
  167. value: '1990-01-01',
  168. defaultValue: new Date(1990, 1, 1),
  169. label: '出生日期 *',
  170. disabledFutureDates: true,
  171. format: 'YYYY/MM/DD',
  172. flexStyle: 'mb-7',
  173. outlined: true,
  174. rules: [v => !!v || '请选择出生日期']
  175. },
  176. {
  177. type: 'combobox',
  178. key: 'enterpriseId',
  179. value: null,
  180. default: null,
  181. label: '任职企业名称 *(可填暂无)',
  182. outlined: true,
  183. clearable: true,
  184. canBeInputted: true, //
  185. itemTextName: 'enterpriseName',
  186. itemText: 'value',
  187. itemValue: 'key',
  188. rules: [v => !!v || '请填写任职企业名称(可填暂无)'],
  189. search: getEnterpriseData,
  190. items: []
  191. },
  192. {
  193. type: 'combobox',
  194. key: 'positionId',
  195. value: null,
  196. default: null,
  197. label: '任职职位名称 *(可填暂无)',
  198. outlined: true,
  199. clearable: true,
  200. canBeInputted: true, //
  201. itemTextName: 'positionName',
  202. itemText: 'nameCn',
  203. itemValue: 'id',
  204. dictTypeName: 'positionSecondData',
  205. rules: [v => !!v || '请填写任职职位名称(可填暂无)'],
  206. search: val => positionName = val,
  207. items: []
  208. },
  209. {
  210. type: 'autocomplete',
  211. key: 'interestedPositionList',
  212. value: null,
  213. default: null,
  214. label: '意向职位 *',
  215. outlined: true,
  216. itemText: 'nameCn',
  217. itemValue: 'id',
  218. multiple: true,
  219. dictTypeName: 'positionSecondData',
  220. rules: [v => !!v || '请选择意向职位'],
  221. items: []
  222. },
  223. {
  224. type: 'autocomplete',
  225. key: 'jobStatus',
  226. value: '',
  227. default: null,
  228. label: '求职状态 *',
  229. outlined: true,
  230. itemText: 'label',
  231. itemValue: 'value',
  232. dictTypeName: 'menduner_job_seek_status',
  233. rules: [v => !!v || '请选择求职状态'],
  234. items: []
  235. },
  236. {
  237. type: 'autocomplete',
  238. key: 'expType',
  239. value: '',
  240. default: null,
  241. label: '工作经验 *',
  242. outlined: true,
  243. itemText: 'label',
  244. itemValue: 'value',
  245. dictTypeName: 'menduner_exp_type',
  246. rules: [v => !!v || '请选择工作经验'],
  247. items: []
  248. },
  249. {
  250. type: 'autocomplete',
  251. key: 'eduType',
  252. value: '',
  253. default: null,
  254. label: '最高学历 *',
  255. outlined: true,
  256. itemText: 'label',
  257. itemValue: 'value',
  258. dictTypeName: 'menduner_education_type',
  259. rules: [v => !!v || '请选择最高学历'],
  260. items: []
  261. },
  262. // label: '学制类型 *', menduner_education_system_type
  263. ]
  264. })
  265. if (import.meta.env.VITE_NODE_ENV === 'production') {
  266. items.value.options = items.value.options.filter(e => e.key !== 'analysis')
  267. }
  268. // 选择文件
  269. const fileInput = ref()
  270. const clicked = ref(false)
  271. const openFileInput = () => {
  272. if (clicked.value) return
  273. clicked.value = true
  274. fileInput.value.click()
  275. clicked.value = false
  276. }
  277. // 上传头像
  278. const accept = ['jpg', 'png', 'jpeg']
  279. const handleUploadFile = async (e) => {
  280. console.log('handleUploadFile:', e)
  281. const file = e.target.files[0]
  282. if (!file) return
  283. const arr = file.name.split('.')
  284. const fileType = arr?.length ? arr[arr.length-1] : ''
  285. if (!accept.includes(fileType)) return Snackbar.warning('请上传图片格式文件')
  286. const size = file.size
  287. if (size / (1024*1024) > 20) {
  288. Snackbar.warning(t('common.fileSizeExceed'))
  289. return
  290. }
  291. const reader = new FileReader()
  292. reader.readAsDataURL(file)
  293. reader.onload = () => {
  294. selectPic.value = String(reader.result)
  295. isShowCopper.value = true
  296. }
  297. }
  298. const handleHideCopper = (data) => {
  299. isShowCopper.value = false
  300. if (data) {
  301. const { file } = data
  302. if (!file) return
  303. const formData = new FormData()
  304. formData.append('file', file)
  305. formData.append('path', 'img')
  306. uploadFile(formData).then(async ({ data }) => {
  307. if (!data) return
  308. items.value.options.find(e => e.key === 'avatar').value = data
  309. })
  310. }
  311. }
  312. // 获取字典内容
  313. const getDictData = async (dictTypeName, key) => {
  314. const item = items.value.options.find(e => e.key === key)
  315. if (item) {
  316. const apiType = dictTypeName === 'positionSecondData' ? 'positionSecondData' : 'dict'
  317. const { data } = await getDict(dictTypeName, apiType === 'dict' ? null : {}, apiType)
  318. item.items = data
  319. // console.log(dictTypeName, '字典内容', data)
  320. }
  321. }
  322. const userInfo = ref(localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')) : {})
  323. const baseInfo = ref(localStorage.getItem('baseInfo') ? JSON.parse(localStorage.getItem('baseInfo')) : {})
  324. items.value.options.forEach((e) => {
  325. if (e.dictTypeName) getDictData(e.dictTypeName, e.key) // 查字典set options
  326. if (baseInfo.value && baseInfo.value[e.key]) e.value = baseInfo.value[e.key] // 人才信息回显
  327. if (userInfo.value && userInfo.value[e.key]) e.value = userInfo.value[e.key] // 人才信息回显
  328. if (e.key === 'sex' && e.value === '0') e.value = e.default
  329. if (setInfo.value[e.key]) e.value = setInfo.value[e.key]
  330. })
  331. // const getName = (obj, key) => {
  332. // const item = items.value.options.find(e => e.key === key)
  333. // if (!item && !item.value) return
  334. // const select = item.items.find(e => item.value === e[item.itemValue || 'value'])
  335. // if (select) {
  336. // obj[item.itemTextName] = select[item.itemText || 'label']
  337. // }
  338. // }
  339. const dealQuery = () => {
  340. query.positionName = positionName || null
  341. if (query.positionId === positionName) delete query.positionId // 有选中id传id和name,否者只传name
  342. query.enterpriseName = enterpriseName || null
  343. if (query.enterpriseId === enterpriseName) delete query.enterpriseId // 有选中id传id和name,否者只传name
  344. //
  345. if (query.interestedPositionList?.length) {
  346. query.interestedList = query.interestedPositionList.map(e => { return {positionId: e} })
  347. }
  348. query.workExpList = [{
  349. enterpriseId: query.enterpriseId,
  350. enterpriseName: query.enterpriseName || null,
  351. positionId: query.positionId,
  352. positionName: query.positionName || null,
  353. }]
  354. }
  355. const getQuery = async () => {
  356. const { valid } = await formPageRef.value.formRef.validate()
  357. if (!valid) return false
  358. const obj = {}
  359. items.value.options.forEach(e => {
  360. if (Object.prototype.hasOwnProperty.call(e, 'data')) return obj[e.key] = e.data
  361. obj[e.key] = e.value === '' ? null : e.value
  362. })
  363. if (!obj.avatar) obj.avatar = getUserAvatar(null, obj.sex)
  364. query = Object.assign(query, obj)
  365. dealQuery()
  366. return query
  367. }
  368. // 填充
  369. const handleAnalyzeFill = (data) => {
  370. // debugger
  371. const person = data?.person || null
  372. if (!person && !Object.keys(person).length) return Snackbar.warning('无可用内容!')
  373. if (data.lastEmployed) {
  374. // debugger
  375. if (data.lastEmployed.enterpriseName) person.enterpriseId = person.enterpriseName = data.lastEmployed.enterpriseName
  376. if (data.lastEmployed.positionName) person.positionId = person.positionName = data.lastEmployed.positionName
  377. if (person.enterpriseName) getEnterpriseData(person.enterpriseName)
  378. }
  379. if (data.lastPositionId) person.interestedPositionList = [data.lastPositionId]
  380. // debugger
  381. items.value.options.forEach((e) => {
  382. if (person[e.key]) e.value = person[e.key]
  383. })
  384. }
  385. const analyzeLoading = ref(false)
  386. const uploadFormRef = ref()
  387. const openUploadDialog = ref(false)
  388. // 上传附件-提交
  389. const uploadFileSubmit = async () => {
  390. const obj = await uploadFormRef.value.getQuery()
  391. if (!obj?.url || !obj?.title) return Snackbar.warning(t('resume.selectResumeToSubmit'))
  392. const query = { title: obj.title, url: obj.url }
  393. analyzeLoading.value = true
  394. await savePersonResumeCv(query)
  395. openUploadDialog.value = false
  396. const data = await resumeParser2({ fileUrl: obj.url })
  397. handleAnalyzeFill(data)
  398. getAttachmentList()
  399. analyzeLoading.value = false
  400. }
  401. handleAnalyzeFill(JSON.parse(JSON.stringify(analyzeTestData)))
  402. const attachmentCount = ref(0)
  403. const getAttachmentList = async () => {
  404. const data = await getPersonResumeCv() // 获取附件
  405. attachmentCount.value = data?.length || 0
  406. }
  407. getAttachmentList()
  408. const handleImportAttachment = async () => {
  409. await getAttachmentList()
  410. if (attachmentCount.value >= 5) return Snackbar.warning('导入解析简历尝试不得超过5次!')
  411. openUploadDialog.value = true
  412. }
  413. defineExpose({
  414. getQuery
  415. })
  416. </script>
  417. <style scoped lang="scss">
  418. .avatarsBox {
  419. height: 80px;
  420. width: 80px;
  421. position: relative;
  422. cursor: pointer;
  423. // margin: 32px;
  424. // margin-right: 40px;
  425. margin: 0 40px 0 32px;
  426. .img {
  427. width: 100%;
  428. height: 100%;
  429. }
  430. .mdi {
  431. font-size: 42px;
  432. color: #fff;
  433. }
  434. div {
  435. position: absolute;
  436. top: 50%;
  437. left: 50%;
  438. transform: translate(-50%, -50%);
  439. border-radius: 50%;
  440. }
  441. }
  442. </style>