|
@@ -1,18 +1,229 @@
|
|
|
<template>
|
|
|
- <div class="d-flex pa-3" style="height: calc(100vh - 50px)">
|
|
|
- <div class="mr-5" style="width: 50%; height: 100%">
|
|
|
- <IFrame v-if="fileUrl" :src="decodeURIComponent(fileUrl)" initHeight="100%"></IFrame>
|
|
|
- </div>
|
|
|
+ <div style="height: calc(100vh - 50px)">
|
|
|
+ <template v-if="showSelect">
|
|
|
+ <div class="d-flex flex-column pb-10" style="width: 800px; min-width: 800px; height: 100%; min-height: 500px; margin: 0 auto;">
|
|
|
+ <attachmentPage analysis @analysis="handleAnalysis"></attachmentPage>
|
|
|
+ </div>
|
|
|
+ <!-- <div class="d-flex justify-center align-center" style="height: 100%">
|
|
|
+ <v-btn color="primary" @click="null">选择已上传简历</v-btn>
|
|
|
+ <v-btn prepend-icon="mdi-upload" class="ml-5" @click="null">上传附件简历</v-btn>
|
|
|
+ </div> -->
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <div class="d-flex pa-3" style="height: 100%">
|
|
|
+ <!-- 简历附件回显 -->
|
|
|
+ <v-card class="d-flex flex-column mr-5 pa-3" style="width: 60%; height: 100%; position: relative;">
|
|
|
+ <v-tabs v-model="tab" align-tabs="start" color="primary" bg-color="#f7f8fa">
|
|
|
+ <v-tab :value="1">查看附件简历</v-tab>
|
|
|
+ <v-tab :value="2">查看文本信息</v-tab>
|
|
|
+ </v-tabs>
|
|
|
+ <div></div>
|
|
|
+ <v-btn style="position: absolute; right: 0;" class="mx-5 mt-2" variant="tonal" @click="showSelect = true">重新选择附件简历</v-btn>
|
|
|
+ <div class="mt-3" style="width: 100%; height: 100%; flex: 1;">
|
|
|
+ <div v-show="tab === 1" style="width: 100%; height: 100%">
|
|
|
+ <IFrame :src="decodeURIComponent(fileUrl)" initHeight="100%"></IFrame>
|
|
|
+ </div>
|
|
|
+ <div v-show="tab === 2">
|
|
|
+ <template v-if="resumeTxt?.length">
|
|
|
+ 简历解析(可复制文本使用)
|
|
|
+ <p v-for="(text, index) in resumeTxt" :key="text + index">{{ text }}</p>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ 无简历解析文本可用
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </v-card>
|
|
|
+ <!-- 解析内容 -->
|
|
|
+ <v-card class="elevation-2 pa-3" style="flex: 1; height: 100%; overflow: hidden; position: relative;" >
|
|
|
+ <div style="height: 100%; overflow: auto;">
|
|
|
+ <div v-for="item of formLIst" :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(item)">删除</v-btn>
|
|
|
+ </div>
|
|
|
+ <component ref="componentRef" :id="item.id" :is="item.path" :data="item.data" />
|
|
|
+ </div>
|
|
|
+ <!-- 保存 -->
|
|
|
+ <div style="position: absolute; bottom: 0; background-color: #fff; z-index: 99; width: 100%; border-top: 1px solid #e4e7eb;" class="py-3 ml-n3 text-center">
|
|
|
+ <v-btn class="buttons mx-3" color="primary" @click="submit">保存</v-btn>
|
|
|
+ <!-- <v-btn class="buttons mx-3" variant="tonal" @click="showSelect = true">重新选择附件简历</v-btn> -->
|
|
|
+ </div>
|
|
|
+ </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>
|
|
|
+ </template>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
defineOptions({ name: 'resume-analysis'})
|
|
|
-import { ref } from 'vue'
|
|
|
+import { shallowRef, ref } from 'vue'
|
|
|
+import { useI18n } from '@/hooks/web/useI18n'
|
|
|
+import IFrame from '@/components/IFrame'
|
|
|
+import avatar from './components/avatar.vue'
|
|
|
+import basicInfo from './components/basicInfo.vue'
|
|
|
+import selfEvaluation from './components/selfEvaluation.vue'
|
|
|
+import educationExp from './components/educationExp.vue'
|
|
|
+import workExperience from './components/workExperience.vue'
|
|
|
+import trainingExperience from './components/trainingExperience.vue'
|
|
|
+import Snackbar from '@/plugins/snackbar'
|
|
|
+import Confirm from '@/plugins/confirm'
|
|
|
+import { saveResumeInfo, resumeParser2 } from '@/api/recruit/personal/resume'
|
|
|
+import { useUserStore } from '@/store/user'
|
|
|
+import attachmentPage from '../attachment'
|
|
|
+const { t } = useI18n()
|
|
|
+const props = defineProps({
|
|
|
+ data: {
|
|
|
+ type: Object,
|
|
|
+ default: () => {}
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const tab = ref(1)
|
|
|
+const exampleList = {
|
|
|
+ avatar: { text: t('resume.avatar'), id: 'avatar', path: avatar },
|
|
|
+ person: { text: t('resume.basicInfo'), id: 'person', path: basicInfo },
|
|
|
+ advantage: { text: t('resume.personalAdvantages'), id: 'advantage', path: selfEvaluation },
|
|
|
+ eduList: { text: t('resume.educationExp'), id: 'eduList', path: educationExp },
|
|
|
+ workList: { text: t('resume.workExperience'), id: 'workList', path: workExperience },
|
|
|
+ trainList: { text: t('resume.trainingExperience'), id: 'trainList', path: trainingExperience },
|
|
|
+}
|
|
|
+
|
|
|
+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
|
|
|
+ let 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]
|
|
|
+ }
|
|
|
+ })
|
|
|
+ const defaultObj = { avatar: "", person: {}, tag: { tagList: [] }, jobInterested: [], eduExp: [], workExp: [], trainExp: [] } // 必传字段
|
|
|
+ obj = { ...defaultObj, ...obj }
|
|
|
+ }
|
|
|
+ // 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'))
|
|
|
+ await useUserStore().getUserBaseInfos() // 更新用户信息
|
|
|
+ setTimeout(() => { window.location.reload() }, 1000)
|
|
|
+ } catch (error) {
|
|
|
+ console.log(error)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const del = (item) => {
|
|
|
+ Confirm('系统提示', `是否确认删除${item.text}?`).then(async () => {
|
|
|
+ formLIst.value = formLIst.value.filter(e => e.id !== item.id)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const resumeTxt = ref([]) // 查看文本信息
|
|
|
+const formLIst = shallowRef([])
|
|
|
+const transformToLIst = async (result) => {
|
|
|
+ formLIst.value = []
|
|
|
+ if (result && Object.keys(result)) {
|
|
|
+ if (result.resume?.rawText) resumeTxt.value = result.resume.rawText.split('\n') || []
|
|
|
+ if (result.person?.advantage) result.advantage = result.person.advantage
|
|
|
+ if (result.person?.avatar) result.avatar = result.person.avatar
|
|
|
+ // obj
|
|
|
+ const dealObjKeys = ['avatar', 'person', 'advantage']
|
|
|
+ dealObjKeys.forEach(key => {
|
|
|
+ if (result[key]) {
|
|
|
+ const obj = {...exampleList[key]}
|
|
|
+ obj.data = result[key]
|
|
|
+ formLIst.value.push(obj)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ // arr
|
|
|
+ const dealArrKeys = ['eduList', 'workList', 'trainList']
|
|
|
+ dealArrKeys.forEach(key => {
|
|
|
+ if (result[key]?.length) {
|
|
|
+ for (let index = 0; index < result[key].length; index++) {
|
|
|
+ const obj = {...exampleList[key]}
|
|
|
+ obj.id = obj.id + '_' + index
|
|
|
+ obj.text = result[key].length > 1 ? obj.text + (index+1) : obj.text
|
|
|
+ obj.data = result[key][index]
|
|
|
+ formLIst.value.push(obj)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+const result = ref({}) // ref(JSON.parse(JSON.stringify(dataObj))) // 测试
|
|
|
+const fileUrl = ref('')
|
|
|
+const showSelect = ref(true)
|
|
|
+const handleAnalysis = async (url) => {
|
|
|
+ url = decodeURIComponent(url)
|
|
|
+ if (!url) return
|
|
|
+ showSelect.value = false
|
|
|
+ loading.value = true
|
|
|
+ const baseUrl = import.meta.env.VITE_PREVIEW_URL
|
|
|
+ fileUrl.value = !url.includes('.pdf') ? `${baseUrl}/onlinePreview?url=${encodeURIComponent(Base64.encode(url))}` : url
|
|
|
+ try {
|
|
|
+ const data = await resumeParser2({ fileUrl: fileUrl.value })
|
|
|
+ result.value = data || {}
|
|
|
+ // result.value = {person: data.person} || {} // 测试
|
|
|
+ await transformToLIst(result.value)
|
|
|
+ } catch (error) {
|
|
|
+ console.log(error)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
-const fileUrl = ref('https://minio.menduner.com/dev/person/1864497212765208578/attachment/85c2977656417e1c30028646b814e4ffcc962ab1ad715816c5f0083630f80289.pdf')
|
|
|
</script>
|
|
|
|
|
|
+
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
</style>
|