index.vue 14 KB

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