LabelingForm.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. <template>
  2. <Dialog title="人才标注" v-model="dialogVisible" class="!w-90%">
  3. <el-row :gutter="20">
  4. <el-col :span="9">
  5. <div v-if="sourceList && sourceList.length > 0" class="!h-80vh overflow-y-auto">
  6. <el-card v-for="val in sourceList" :key="val.id" class="mb-10px">
  7. <template #header>
  8. <CardTitle :title="typeObj[val.task_type] + val.source_date" />
  9. </template>
  10. <el-image
  11. v-if="['名片', '杂项'].includes(val.task_type)"
  12. width="100%"
  13. :preview-src-list="[val.minio_path]"
  14. class="cursor-pointer"
  15. :src="val.minio_path"
  16. />
  17. <!-- 门墩儿招聘 -->
  18. <template v-if="val.task_type === '招聘'">
  19. <el-tabs v-model="activeName" type="border-card">
  20. <el-tab-pane label="基本信息" name="info">
  21. <Info :id="personId" :user-id="userId" />
  22. <expExtend :user-id="userId" defaultShowAll class="m-t-20px" />
  23. </el-tab-pane>
  24. <el-tab-pane label="附件简历" name="Attachment">
  25. <Attachment showPreview :user-id="userId" />
  26. </el-tab-pane>
  27. </el-tabs>
  28. </template>
  29. <template v-if="val.task_type === '简历'">
  30. <IFrame :src="val.minio_path" />
  31. </template>
  32. <template v-if="val.task_type === '新任命'">
  33. <iframe
  34. :id="val.id"
  35. class="markdownContent"
  36. src=""
  37. frameborder="0"
  38. ></iframe>
  39. </template>
  40. </el-card>
  41. </div>
  42. <el-card v-else>
  43. <el-empty description="暂无来源" />
  44. </el-card>
  45. </el-col>
  46. <el-col :span="15">
  47. <div class="!h-100% overflow-y-auto">
  48. <el-tabs type="border-card">
  49. <el-tab-pane label="人才信息">
  50. <FormPage ref="baseInfoRef" formType="edit" :itemData="talentItem" />
  51. <div class="text-right mt-12px">
  52. <el-button @click="handleSave" type="primary" :disabled="saveLoading">保 存</el-button>
  53. <el-button @click="dialogVisible = false">取消</el-button>
  54. </div>
  55. </el-tab-pane>
  56. <el-tab-pane label="人才标签">
  57. <el-card shadow="never">
  58. <div class="my-5">
  59. <div class="mt-4 px-3 pb-3" style="border: 1px dashed #67c23a; border-radius: 4px;">
  60. <div v-if="talentSelectedTags?.length">
  61. <el-tag
  62. v-for="(item, index) in talentSelectedTags"
  63. :key="index"
  64. closable
  65. size="large"
  66. type="success"
  67. class="mr-14px mt-14px"
  68. @close="closeClick(item)"
  69. >
  70. {{ item.name }}
  71. </el-tag>
  72. </div>
  73. <div v-else style="color: #777; text-align: center; line-height: 50px; margin-top: 12px;">请添加标签</div>
  74. </div>
  75. <div :class="{'mt-5': talentSelectedTags?.length > 0}">
  76. <el-tag
  77. v-for="(item, index) in tagList" :key="index"
  78. size="large"
  79. type="primary"
  80. class="mr-14px mt-14px"
  81. :class="{'cursor-pointer': !talentSelectedTags.find(k => k.name === item.name)}"
  82. :effect="talentSelectedTags.find(k => k.name === item.name) ? 'info' : 'default'"
  83. @click="handleAdd(item)"
  84. >
  85. + {{ item.name }}
  86. </el-tag>
  87. </div>
  88. </div>
  89. </el-card>
  90. <div class="text-right mt-12px">
  91. <el-button @click="submitForm" type="primary" :disabled="loading">保 存</el-button>
  92. <el-button @click="dialogVisible = false">取消</el-button>
  93. </div>
  94. </el-tab-pane>
  95. </el-tabs>
  96. </div>
  97. </el-col>
  98. </el-row>
  99. </Dialog>
  100. </template>
  101. <script setup>
  102. import { talentLabelingApi } from '@/api/menduner/system/talentMap/labeling'
  103. import { talentTagApi } from '@/api/menduner/system/talentMap/tag'
  104. import { getDict } from '@/hooks/web/useDictionaries'
  105. import { cloneDeep } from 'lodash-es'
  106. import { TalentMap } from '@/api/menduner/system/talentMap'
  107. import FormPage from '@/views/menduner/system/talentMap/components/FormPage.vue'
  108. import { marked } from 'marked'
  109. import { generateUUID } from '@/utils'
  110. import Info from '@/views/menduner/system/person/details/components/info.vue'
  111. import expExtend from '@/views/menduner/system/person/details/components/expExtend.vue'
  112. import Attachment from '@/views/menduner/system/person/details/components/attachment.vue'
  113. const message = useMessage() // 消息弹窗
  114. const loading = ref(false)
  115. const dialogVisible = ref(false) // 弹窗的是否展示
  116. const talentItem = ref({})
  117. const talentSelectedTags = ref([])
  118. const tagList = ref([])
  119. const typeObj = {
  120. '招聘': '门墩儿招聘',
  121. '新任命': '门墩儿新任命',
  122. '名片': '名片',
  123. '简历': '简历',
  124. '杂项': '杂项'
  125. }
  126. // 获取人才标签
  127. const getTagList = async () => {
  128. loading.value = true
  129. try {
  130. const data = await talentTagApi.getTalentTagList()
  131. tagList.value = data || []
  132. } finally {
  133. loading.value = false
  134. }
  135. }
  136. // 获取人才标签
  137. const getTalentTagById = async() => {
  138. const id = talentItem.value?.id
  139. if (!id) {
  140. talentSelectedTags.value = []
  141. return
  142. }
  143. const tagData = await talentLabelingApi.getTalentTagById(id)
  144. talentSelectedTags.value = tagData ? tagData.map((i) => {
  145. return { id: i.talent, name: i.tag }
  146. }) : []
  147. }
  148. // markdown回显
  149. const showPage = (id, html) => {
  150. // 将 data-src 转化为 src
  151. html = html.replace(/data-src/g, 'src')
  152. .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/g, '')
  153. .replace(/https/g, 'http')
  154. nextTick(() => {
  155. const iframe = document.getElementById(id)
  156. if (!iframe) return
  157. const doc = iframe.contentDocument || iframe.document
  158. // 设置 iframe 中请求不发送 referrer,以绕过图片防盗链
  159. const htmlArr = html.split('</head>')
  160. const html_src_add = htmlArr[0] + '<meta name="referrer" content="never"></head>' + htmlArr[1]
  161. doc.open()
  162. doc.write(html_src_add)
  163. doc.close()
  164. // 设置图片宽高
  165. let iwindow = iframe.contentWindow;
  166. iframe.addEventListener('load',function () {
  167. let idoc = iwindow.document;
  168. let imgs = idoc.getElementsByTagName('img')
  169. for (let i = 0; i < imgs.length; i++) {
  170. const img = imgs[i]
  171. if (img) {
  172. if (img.width >= img.height) {
  173. img.width = iframe.clientWidth / 2
  174. } else {
  175. img.height = iframe.clientHeight / 2
  176. let left = (iframe.clientWidth - img.width) / 2
  177. img.style.marginLeft = left + "px"
  178. }
  179. }
  180. }
  181. })
  182. })
  183. }
  184. const handleShowPage = async (id, url) => {
  185. await nextTick()
  186. const response = await fetch(url)
  187. const text = await response.text()
  188. const doc = `
  189. <!DOCTYPE html>
  190. <html class="">
  191. <head></head>
  192. <body>
  193. ${marked(text)}
  194. </body>
  195. </html>
  196. `
  197. showPage(id, doc)
  198. }
  199. /** 打开弹窗 */
  200. const personId = ref('')
  201. const userId = ref('')
  202. const activeName = ref('info')
  203. const sourceList = ref([])
  204. const open = async (data) => {
  205. sourceList.value = []
  206. personId.value = null
  207. userId.value = null
  208. dialogVisible.value = true
  209. talentItem.value = data
  210. if (data?.origin_source) {
  211. const list = typeof data.origin_source === 'string' ? JSON.parse(data.origin_source) : data.origin_source
  212. // 时间排序
  213. const sortedList = list.sort((a, b) => new Date(b.source_date) - new Date(a.source_date))
  214. sourceList.value = sortedList.map(e => {
  215. const item = { ...e, id: generateUUID() }
  216. if (e.task_type === '新任命') {
  217. handleShowPage(item.id, item.minio_path)
  218. }
  219. if (e.task_type === '招聘') { // 门墩儿招聘-人员信息在组件中通过id和userId获取
  220. const obj = typeof e.minio_path === 'string' ? JSON.parse(e.minio_path) : e.minio_path
  221. personId.value = obj?.id || ''
  222. userId.value = obj?.userId || ''
  223. activeName.value = 'info'
  224. }
  225. return item
  226. })
  227. }
  228. // 获取所有人才标签
  229. await getTagList()
  230. // 获取人才标签
  231. await getTalentTagById()
  232. }
  233. defineExpose({ open }) // 提供 open 方法,用于打开弹窗
  234. // 标签删除
  235. const closeClick = (item) => {
  236. const index = talentSelectedTags.value.findIndex((i) => i === item)
  237. if (index !== -1) talentSelectedTags.value.splice(index, 1)
  238. }
  239. // 标签添加
  240. const handleAdd = (item) => {
  241. talentSelectedTags.value.push(item)
  242. }
  243. /** 人才标签保存 */
  244. const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
  245. const submitForm = async () => {
  246. if (!talentSelectedTags.value || !talentSelectedTags.value.length) return message.warning('请选择要更新的人才标签')
  247. loading.value = true
  248. const tags = talentSelectedTags.value.map(e => {
  249. return { talent: talentItem.value.id, tag: e.name }
  250. })
  251. // 提交请求
  252. try {
  253. await talentLabelingApi.updateTalentTags(tags)
  254. message.success('人才标签更新成功')
  255. dialogVisible.value = false
  256. // 发送操作成功的事件
  257. emit('success')
  258. } finally {
  259. loading.value = false
  260. }
  261. }
  262. // 保存人才信息
  263. const baseInfoRef = ref(null)
  264. const saveLoading = ref(false)
  265. const handleSave = async () => {
  266. saveLoading.value = true
  267. const formQuery = baseInfoRef.value.formQuery
  268. if (!formQuery || !Object.keys(formQuery).length) return saveLoading.value = true
  269. // 数组转为字符串保存
  270. if (Array.isArray(formQuery?.mobile)) {
  271. formQuery.mobile = formQuery.mobile.filter(i => Boolean(i)).map(j => String(j).replace(/,|,/g, '')).join(',');
  272. }
  273. const params = {
  274. ...formQuery,
  275. origin_source: talentItem.value?.origin_source,
  276. image_path: talentItem.value?.image_path
  277. }
  278. try {
  279. await talentLabelingApi.updateBusinessCard(params, talentItem.value.id)
  280. dialogVisible.value = false
  281. message.success('保存成功')
  282. emit('success')
  283. } finally {
  284. saveLoading.value = false
  285. }
  286. }
  287. </script>
  288. <style scoped lang="scss">
  289. .base-info {
  290. background-color: #f7f8fa;
  291. border-radius: 6px;
  292. padding: 15px;
  293. }
  294. :deep {
  295. .el-descriptions__content {
  296. color: #303133;
  297. }
  298. .el-descriptions__body {
  299. background-color: #f7f8fa;
  300. }
  301. .markdownContent {
  302. width: 100%;
  303. max-width: 100%;
  304. height: 600px;
  305. border: none;
  306. overflow: hidden;
  307. }
  308. }
  309. </style>