index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. <template>
  2. <!-- 搜索工作栏 -->
  3. <ContentWrap>
  4. <el-form
  5. class="-mb-15px"
  6. :model="queryParams"
  7. ref="queryFormRef"
  8. :inline="true"
  9. label-width="68px"
  10. >
  11. <el-form-item label="任务类型" prop="task_type">
  12. <el-select
  13. v-model="queryParams.task_type"
  14. placeholder="请选择类型"
  15. clearable
  16. class="!w-200px"
  17. @change="handleQuery"
  18. >
  19. <el-option v-for="(val, index) in taskType" :label="val.label" :value="val.value" :key="index" />
  20. </el-select>
  21. </el-form-item>
  22. <el-form-item label="任务状态" prop="task_status">
  23. <el-select
  24. v-model="queryParams.task_status"
  25. placeholder="请选择状态"
  26. clearable
  27. class="!w-200px"
  28. @change="handleQuery"
  29. >
  30. <el-option v-for="(val, index) in taskStatus" :label="val" :value="val" :key="index" />
  31. </el-select>
  32. </el-form-item>
  33. <el-form-item>
  34. <el-button @click="handleQuery"><Icon icon="ep:search" /> 搜索</el-button>
  35. <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
  36. <el-button type="primary" plain @click="handleAdd">
  37. <Icon icon="ep:plus" class="mr-5px" /> 新增人才
  38. </el-button>
  39. </el-form-item>
  40. </el-form>
  41. </ContentWrap>
  42. <!-- 列表 -->
  43. <ContentWrap>
  44. <el-table v-loading="loading" :data="list" :stripe="true">
  45. <el-table-column label="任务名称" align="center" prop="task_name" />
  46. <el-table-column label="任务类型" align="center" prop="task_type" />
  47. <el-table-column label="任务状态" align="center" prop="task_status" />
  48. <el-table-column label="创建时间" align="center" prop="created_at" :formatter="dateFormatter" />
  49. <el-table-column label="更新时间" align="center" prop="updated_at" :formatter="dateFormatter" />
  50. <el-table-column label="操作" align="center" fixed="right" min-width="110">
  51. <template #default="scope">
  52. <el-button
  53. v-if="scope.row.task_status === '待解析'"
  54. link
  55. type="success"
  56. @click="handleAnalysis(scope.row)"
  57. >解析</el-button>
  58. <el-button
  59. v-if="scope.row.task_status === '成功' && scope.row.parse_result"
  60. link
  61. type="primary"
  62. @click="handleStore(scope.row)"
  63. >入库</el-button>
  64. </template>
  65. </el-table-column>
  66. </el-table>
  67. <!-- 分页 -->
  68. <Pagination
  69. :total="total"
  70. v-model:page="queryParams.page"
  71. v-model:limit="queryParams.per_page"
  72. @pagination="getList"
  73. />
  74. </ContentWrap>
  75. <!-- 选择来源 -->
  76. <Dialog title="新增" v-model="openSelect" width="550" @close="openSelect = false">
  77. <el-radio-group v-model="radioValue" size="large" class="radioBox">
  78. <el-radio
  79. v-for="item in radioList"
  80. :key="item.value"
  81. :value="item.value"
  82. >
  83. {{ item.label }}
  84. </el-radio>
  85. </el-radio-group>
  86. <template #footer>
  87. <el-button type="primary" @click="handleSelect">确 认</el-button>
  88. <el-button @click="openSelect = false">取 消</el-button>
  89. </template>
  90. </Dialog>
  91. <!-- 解析文件上传 -->
  92. <Dialog :title="radioObject[radioValue]" v-model="dialog_upload" :modalClose="false" :width="DialogWidth" @close="handleCancel">
  93. <div>
  94. <!-- 简历解析 -->
  95. <template v-if="radioValue === 'file'">
  96. <el-upload
  97. ref="uploadRef"
  98. v-model:file-list="fileList"
  99. :action="uploadUrl"
  100. :auto-upload="false"
  101. :data="fileData"
  102. :on-change="handleChange"
  103. :on-error="submitFormError"
  104. :on-exceed="handleExceed"
  105. :http-request="httpRequest"
  106. accept=".pdf"
  107. drag
  108. multiple
  109. class="flex-1"
  110. >
  111. <i class="el-icon-upload"></i>
  112. <div class="el-upload__text">上传附件, 将文件拖到此处,或 <em>点击上传</em></div>
  113. <template #tip>
  114. <div class="el-upload__tip color-red">
  115. 提示:仅允许导入 PDF格式文件!
  116. </div>
  117. </template>
  118. </el-upload>
  119. </template>
  120. <!-- 名片解析 -->
  121. <template v-if="radioValue === 'card'">
  122. <UploadImgs
  123. v-model="cardImgUrl"
  124. :uploadSuccessTip="false"
  125. :limit="30"
  126. @handle-change="cardUploadChange"
  127. height="150px" width="150px" style="margin: 20px auto; text-align: center;"
  128. >
  129. <template #tip>{{ cardImgUrl ? '' : '请上传名片' }}</template>
  130. </UploadImgs>
  131. </template>
  132. <!-- 杂项 -->
  133. <template v-if="radioValue === 'picture'">
  134. <div class="flex align-center">
  135. <UploadImg
  136. v-model="cardImgUrl"
  137. :uploadSuccessTip="false"
  138. @handle-change="val => cardUploadChange(val, '杂项')"
  139. height="150px" width="150px" style="margin: 20px auto; text-align: center;"
  140. >
  141. <template #tip>{{ cardImgUrl ? '' : '请上传图片' }}</template>
  142. </UploadImg>
  143. </div>
  144. </template>
  145. <!-- 网页解析 -->
  146. <template v-if="radioValue === 'web'">
  147. <webAnalysis
  148. @analysis="handleWebAnalysis"
  149. @reset="webOriginList = []"
  150. />
  151. </template>
  152. <!-- 网页解析 -->
  153. <template v-if="radioValue === 'menduner'">
  154. <Search ref="SearchRef" />
  155. </template>
  156. </div>
  157. <template #footer>
  158. <el-button @click="handleSubmit" type="success" :disabled="analysisLoading" :loading="analysisLoading">提 交</el-button>
  159. <el-button @click="handleCancel">取 消</el-button>
  160. </template>
  161. </Dialog>
  162. <StorePage ref="StorePageRef" @refresh="getList" />
  163. </template>
  164. <script setup>
  165. defineOptions({ name: 'TalentMapStoreIndex' })
  166. import { dateFormatter } from '@/utils/formatTime'
  167. import { talentLabelingApi } from '@/api/menduner/system/talentMap/labeling'
  168. import { TalentMap } from '@/api/menduner/system/talentMap'
  169. import { Plus } from '@element-plus/icons-vue'
  170. import { timesTampChange, timestampToAge } from '@/utils/transform/date'
  171. import Search from './components/search.vue'
  172. import webAnalysis from './components/webAnalysis.vue'
  173. import { useUpload } from '@/components/UploadFile/src/useUpload'
  174. import { commonApi } from '@/api/menduner/common'
  175. import { Base64 } from 'js-base64'
  176. import Info from '@/views/menduner/system/person/details/components/info.vue'
  177. import expExtend from '@/views/menduner/system/person/details/components/expExtend.vue'
  178. import Attachment from '@/views/menduner/system/person/details/components/attachment.vue'
  179. import { talentWebParsingApi } from '@/api/menduner/system/talentMap/webParsing'
  180. import { ElLoading } from 'element-plus'
  181. import StorePage from '@/views/menduner/system/talentMap/maintenance/gather/components/store.vue'
  182. import { talentGatherApi } from '@/api/menduner/system/talentMap/gather'
  183. const { uploadUrl, httpRequest } = useUpload()
  184. const message = useMessage() // 消息弹窗
  185. const { t } = useI18n() // 国际化
  186. const loading = ref(false) // 列表的加载中
  187. const list = ref([]) // 列表的数据
  188. const total = ref(0) // 列表的总页数
  189. const queryParams = reactive({
  190. page: 1,
  191. per_page: 10,
  192. task_type: undefined, // 任务类型
  193. task_status: undefined // 任务状态
  194. })
  195. const queryFormRef = ref() // 搜索的表单
  196. const dialog_upload = ref(false)
  197. const taskType = [
  198. { label: '名片', value: '名片' },
  199. { label: '简历', value: '简历' },
  200. { label: '门墩儿新任命', value: '新任命' },
  201. { label: '门墩儿招聘', value: '招聘' },
  202. { label: '杂项', value: '杂项' }
  203. ]
  204. const taskStatus = ['待解析', '成功']
  205. const itemData = ref({})
  206. const SearchRef = ref(null)
  207. /** 查询列表 */
  208. const getList = async () => {
  209. loading.value = true
  210. try {
  211. list.value = []
  212. const data = await talentGatherApi.getTaskList(queryParams)
  213. list.value = data.tasks ?? []
  214. total.value = data.pagination.total ?? 0
  215. } finally {
  216. loading.value = false
  217. }
  218. }
  219. /** 搜索按钮操作 */
  220. const handleQuery = () => {
  221. queryParams.page = 1
  222. getList()
  223. }
  224. /** 重置按钮操作 */
  225. const resetQuery = () => {
  226. queryFormRef.value.resetFields()
  227. handleQuery()
  228. }
  229. // 网页解析-信息提取
  230. const webOriginList = ref([])
  231. const handleWebAnalysis = (list) => {
  232. webOriginList.value = list ?? []
  233. }
  234. // 入库
  235. const StorePageRef = ref(null)
  236. const handleStore = async (row) => {
  237. // const { task_type, task_name, parse_result, summary, processed_time } = row
  238. // const { results } = parse_result
  239. // if (!results || !results.length) return message.warning('该任务无解析数据,无法入库')
  240. // StorePageRef.value.open(task_type, results, { summary, processed_time })
  241. if (!row?.task_name) return message.warning('该任务无解析数据,无法入库')
  242. StorePageRef.value.open(row.task_name)
  243. }
  244. // 任务解析
  245. const handleAnalysis = async ({ task_type, id, task_source }) => {
  246. if (!id) return
  247. await message.confirm('是否解析当前任务?')
  248. const loading = ElLoading.service({
  249. lock: true,
  250. text: '任务解析中...',
  251. background: 'rgba(0, 0, 0, 0.7)',
  252. })
  253. const params = {
  254. task_type,
  255. id,
  256. data: task_source.minio_paths_json
  257. }
  258. if (task_type === '杂项') params.process_type = 'table'
  259. if (task_type === '新任命') params.publish_time = '2025-07-21'
  260. // 招聘数据未返回,params.data设置为测试数据
  261. if (task_type === '招聘' && !params.data?.length) params.data = [{"id":"1843906600113389570","userId":"598525227328212992","name":"尹世丹","name_zh":"尹世丹"},{"id":"1942125259693166593","userId":"1942124499676893185","name":"陈梅","name_zh":"陈梅"}]
  262. try {
  263. await talentGatherApi.taskAnalysis(params)
  264. message.success('解析成功')
  265. getList()
  266. } finally {
  267. loading.close()
  268. }
  269. }
  270. // 关闭上传弹窗
  271. const handleCancel = () => {
  272. dialog_upload.value = false
  273. analysisLoading.value = false
  274. cardUploadRow.value = []
  275. fileList.value = []
  276. }
  277. // 创建任务
  278. const handleCreateTask = async (query) => {
  279. try {
  280. await talentGatherApi.createTask(query)
  281. message.success('任务创建成功')
  282. getList()
  283. } finally {
  284. analysisLoading.value = false
  285. webOriginList.value = []
  286. handleCancel()
  287. }
  288. }
  289. // 新任命
  290. const handleCreateWebTask = async () => {
  291. if (!webOriginList.value || !webOriginList.value?.length) return message.warning('请输入新任命链接查看后再进行提交')
  292. analysisLoading.value = true
  293. const formData = new FormData()
  294. formData.append('task_type', '新任命')
  295. webOriginList.value.forEach(e => {
  296. formData.append('files', e.file)
  297. })
  298. handleCreateTask(formData)
  299. }
  300. // 名片、简历、杂项
  301. const analysisLoading = ref(false)
  302. const handleSubmit = async () => {
  303. const type = radioValue.value
  304. // 新任命任务创建
  305. if (type === 'web') return handleCreateWebTask()
  306. if (['card', 'file', 'picture'].includes(type)) {
  307. const currentList = type === 'file' ? fileList.value : cardUploadRow.value
  308. if (!currentList || !currentList?.length) {
  309. message.warning(`请上传${type === 'file' ? '简历' : type === 'card' ? '名片' : '杂项'}`)
  310. return
  311. }
  312. analysisLoading.value = true
  313. const formData = new FormData()
  314. formData.append('task_type', type === 'file' ? '简历' : type === 'card' ? '名片' : '杂项')
  315. currentList.forEach(e => {
  316. formData.append('files', e.raw)
  317. })
  318. handleCreateTask(formData)
  319. }
  320. if (type === 'menduner') {
  321. const formData = new FormData()
  322. const list = SearchRef.value?.addList?.length ? SearchRef.value.addList : null
  323. if (!list) return message.warning('请先选择人员 !')
  324. formData.append('task_type', '招聘')
  325. formData.append('data', JSON.stringify(list))
  326. handleCreateTask(formData)
  327. }
  328. }
  329. // 简历解析
  330. const uploadRef = ref()
  331. const fileList = ref([])
  332. const fileData = ref({ path: '' })
  333. // 文件上传
  334. const handleChange = async (file) => {
  335. fileData.value.path = file.name
  336. unref(uploadRef)?.submit()
  337. }
  338. const submitFormError = () => {
  339. message.error('上传失败,请您重新上传!')
  340. }
  341. const handleExceed = () => {
  342. message.error('最多只能上传一个文件!')
  343. }
  344. // 简历解析数据解构赋值
  345. const resumeAnalysisToForm = (data) => {
  346. itemData.value = {
  347. name_zh: data?.person?.name || '',
  348. email: data?.person?.email || '',
  349. mobile: data?.person?.phone || '',
  350. birthday: data?.person?.birthday ? timesTampChange(data.person.birthday, 'Y-M-D') : '',
  351. age: data?.person?.birthday ? timestampToAge(data.person.birthday) : null,
  352. career_path: data?.workList ? data.workList.map(e => {
  353. return {
  354. hotel_zh: e?.enterpriseName || null,
  355. title_zh: e?.positionName || null,
  356. date: e?.startTime ? timesTampChange(e.startTime, 'Y-M-D') : null
  357. }
  358. }) : null,
  359. created_at: data?.person?.createTime ? timesTampChange(data.person.createTime, 'Y-M-D') : null,
  360. updated_at: data?.person?.updateTime ? timesTampChange(data.person.updateTime, 'Y-M-D') : null,
  361. }
  362. }
  363. // 名片解析
  364. const cardUploadRow = ref(null)
  365. const cardImgUrl = ref(null)
  366. const cardUploadChange = (file, type) => {
  367. cardUploadRow.value = type ? [{ raw: file }] : file
  368. }
  369. // 选择解析方式
  370. const DialogWidth = ref('500')
  371. const handleSelect = () => {
  372. openSelect.value = false
  373. itemData.value = {}
  374. DialogWidth.value = '500'
  375. const type = radioValue.value
  376. if (['card', 'web'].includes(type)) {
  377. DialogWidth.value = '800'
  378. }
  379. if (type === 'menduner') {
  380. DialogWidth.value = '1200'
  381. }
  382. dialog_upload.value = true
  383. }
  384. const openSelect = ref(false)
  385. const radioObject = { card: '名片', file: '简历', web: '门墩儿新任命', menduner: '门墩儿招聘', picture: '杂项' }
  386. const radioList = ref(Object.keys(radioObject).map(key => ({ value: key, label: radioObject[key]}) ))
  387. const defaultValue = radioList.value[0].value // 默认选中
  388. const radioValue = ref(defaultValue)
  389. // 新增解析
  390. const handleAdd = () => {
  391. webOriginList.value = []
  392. cardUploadRow.value = null
  393. cardImgUrl.value = null
  394. analysisLoading.value = false
  395. radioValue.value = defaultValue // 重置解析类型
  396. openSelect.value = true
  397. }
  398. /** 初始化 **/
  399. onMounted(() => {
  400. getList()
  401. })
  402. </script>
  403. <style lang="scss" scoped>
  404. .radioBox {
  405. margin: 40px 0;
  406. }
  407. :deep {
  408. .el-tag__content {
  409. display: flex;
  410. align-items: center;
  411. }
  412. }
  413. </style>