index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. <template>
  2. <ContentWrap>
  3. <!-- 搜索工作栏 -->
  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="name">
  12. <el-input v-model="queryParams.name" placeholder="请输入名称" clearable @keyup.enter="handleQuery" class="!w-180px" />
  13. </el-form-item>
  14. <el-form-item>
  15. <el-button @click="handleQuery('search')"><Icon icon="ep:search" /> 搜索</el-button>
  16. <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
  17. <el-button type="primary" plain @click="handleAdd">
  18. <Icon icon="ep:plus" class="mr-5px" /> 新增解析
  19. </el-button>
  20. </el-form-item>
  21. </el-form>
  22. </ContentWrap>
  23. <!-- 列表 -->
  24. <ContentWrap>
  25. <el-table v-loading="loading" :data="list" :stripe="true">
  26. <el-table-column label="姓名(中)" align="center" prop="name_zh" fixed="left" />
  27. <el-table-column label="姓名(英)" align="center" prop="name_en" />
  28. <el-table-column label="职位" align="center" prop="title_zh" />
  29. <el-table-column label="酒店/公司" align="center" prop="hotel_zh" />
  30. <el-table-column label="手机号码" align="center" prop="phone" />
  31. <el-table-column label="固定电话" align="center" prop="mobile" />
  32. <el-table-column label="创建日期" align="center" prop="created_at" :formatter="dateFormatter" />
  33. <el-table-column label="状态" align="center" prop="status" width="80">
  34. <template #default="scope">
  35. <el-tag type="success" v-if="scope.row.status === 'active'">已启用</el-tag>
  36. <el-tag type="danger" v-else>已禁用</el-tag>
  37. </template>
  38. </el-table-column>
  39. <el-table-column label="操作" align="center" fixed="right" min-width="110">
  40. <template #default="scope">
  41. <el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
  42. <el-button link type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
  43. <el-button link :type="scope.row.status === 'active' ? 'danger': 'success'" @click="handleDisable(scope.row)">
  44. {{ scope.row.status === 'active' ? '禁用' : '启用'}}
  45. </el-button>
  46. </template>
  47. </el-table-column>
  48. </el-table>
  49. <!-- 上传 -->
  50. <Dialog title="名片解析" v-model="openUploadImg" width="500" @close="handleCancel">
  51. <UploadImg
  52. v-model="filePath"
  53. :limit="1"
  54. :uploadSuccessTip="false"
  55. @handle-change="uploadChange"
  56. height="150px" width="150px" style="margin: 20px auto; width: 150px;"
  57. >
  58. <template #tip>{{ filePath ? '' : '请上传名片' }}</template>
  59. </UploadImg>
  60. <template #footer>
  61. <el-button @click="handleAnalysis" type="success" :disabled="analysisLoading" :loading="analysisLoading">解 析</el-button>
  62. <el-button @click="handleCancel">取 消</el-button>
  63. </template>
  64. </Dialog>
  65. <!-- 解析回显 -->
  66. <Dialog title="名片解析" v-model="showAnalysisTable" width="80%">
  67. <div class="analysisInfoBox">
  68. <div class="image">
  69. <el-image v-if="filePath" class="!w-100%" :src="filePath" />
  70. <div v-else>
  71. <UploadImg
  72. v-model="filePath"
  73. :limit="1"
  74. :uploadSuccessTip="false"
  75. drag
  76. buttonUpload
  77. @handle-change="uploadChange"
  78. height="32px" width="104px"
  79. style="margin: 0 auto; width: 104px;margin-top: 40%;"
  80. >
  81. <template #tip>{{ filePath ? '' : '请上传名片' }}</template>
  82. </UploadImg>
  83. </div>
  84. </div>
  85. <div class="formBox">
  86. <el-form
  87. ref="formRef"
  88. :model="formQuery"
  89. label-width="128px"
  90. v-loading="formLoading"
  91. >
  92. <el-row>
  93. <div class="m-title">基础信息</div>
  94. </el-row>
  95. <el-row :gutter="10">
  96. <el-col :span="12">
  97. <el-form-item label="姓名(中)" prop="name_zh">
  98. <el-input v-model="formQuery.name_zh" placeholder="请输入中文姓名" />
  99. </el-form-item>
  100. </el-col>
  101. <el-col :span="12">
  102. <el-form-item label="姓名(英)" prop="name_en">
  103. <el-input v-model="formQuery.name_en" placeholder="请输入英文姓名" />
  104. </el-form-item>
  105. </el-col>
  106. </el-row>
  107. <el-row :gutter="10">
  108. <el-col :span="12">
  109. <el-form-item label="职位/头衔(中)" prop="title_zh">
  110. <el-input v-model="formQuery.title_zh" placeholder="请输入中文职位/头衔" />
  111. </el-form-item>
  112. </el-col>
  113. <el-col :span="12">
  114. <el-form-item label="职位/头衔(英)" prop="title_en">
  115. <el-input v-model="formQuery.title_en" placeholder="请输入英文职位/头衔" />
  116. </el-form-item>
  117. </el-col>
  118. </el-row>
  119. <el-row :gutter="10">
  120. <el-col :span="24">
  121. <el-form-item label="生日" prop="birthday">
  122. <el-input v-model="formQuery.birthday" placeholder="请输入出生日期" />
  123. </el-form-item>
  124. </el-col>
  125. </el-row>
  126. <el-row :gutter="10">
  127. <el-col :span="24">
  128. <el-form-item label="居住地" prop="residence">
  129. <el-input v-model="formQuery.residence" placeholder="请输入当前居住地址" />
  130. </el-form-item>
  131. </el-col>
  132. </el-row>
  133. <el-row>
  134. <div class="m-title">联系方式</div>
  135. </el-row>
  136. <el-row :gutter="10">
  137. <el-col :span="12">
  138. <el-form-item label="手机号码" prop="mobile">
  139. <el-input v-model="formQuery.mobile" placeholder="请输入手机号码" />
  140. </el-form-item>
  141. </el-col>
  142. <el-col :span="12">
  143. <el-form-item label="固定电话" prop="phone">
  144. <el-input v-model="formQuery.phone" placeholder="请输入固定电话" />
  145. </el-form-item>
  146. </el-col>
  147. </el-row>
  148. <el-form-item label="电子邮箱" prop="email">
  149. <el-input v-model="formQuery.email" placeholder="请输入电子邮箱" />
  150. </el-form-item>
  151. <el-row>
  152. <div class="m-title">酒店/公司信息</div>
  153. </el-row>
  154. <el-form-item label="酒店/公司名称(中)" prop="hotel_zh">
  155. <el-input v-model="formQuery.hotel_zh" placeholder="请输入中文酒店/公司名称" />
  156. </el-form-item>
  157. <el-form-item label="酒店/公司名称(英)" prop="hotel_en">
  158. <el-input v-model="formQuery.hotel_en" placeholder="请输入英文酒店/公司名称" />
  159. </el-form-item>
  160. <el-form-item label="品牌名称(中)" prop="brand_zh">
  161. <el-input v-model="formQuery.brand_zh" placeholder="请输入中文品牌名称" />
  162. </el-form-item>
  163. <el-form-item label="品牌名称(英)" prop="brand_en">
  164. <el-input v-model="formQuery.brand_en" placeholder="请输入英文品牌名称" />
  165. </el-form-item>
  166. <el-form-item label="隶属关系(中)" prop="affiliation_zh">
  167. <el-input v-model="formQuery.affiliation_zh" placeholder="请输入中文隶属关系" />
  168. </el-form-item>
  169. <el-form-item label="隶属关系(英)" prop="affiliation_en">
  170. <el-input v-model="formQuery.affiliation_en" placeholder="请输入英文隶属关系" />
  171. </el-form-item>
  172. <el-form-item label="品牌组合" prop="brand_group">
  173. <el-input v-model="formQuery.brand_group" placeholder="请输入品牌组合" />
  174. </el-form-item>
  175. <el-row>
  176. <div class="m-title">职业轨迹</div>
  177. </el-row>
  178. <el-row :gutter="10" class="trajectoryBox" v-for="(item, index) of careerTrajectory" :key="'trajectory' + index">
  179. <el-col :span="20">
  180. <el-form-item label="酒店名称" prop="hotel_zh" label-width="128px">
  181. <el-input v-model="item.hotel_zh" placeholder="请输入酒店名称" />
  182. </el-form-item>
  183. <el-form-item label="职位名称" prop="title_zh" label-width="128px">
  184. <el-input v-model="item.title_zh" placeholder="请输入职位名称" />
  185. </el-form-item>
  186. <el-form-item label="任职时间" prop="date">
  187. <el-date-picker
  188. v-model="item.date"
  189. value-format="YYYY-MM-DD"
  190. type="date"
  191. start-placeholder="开始日期"
  192. end-placeholder="结束日期"
  193. />
  194. </el-form-item>
  195. </el-col>
  196. <el-col :span="4">
  197. <div class="flex justify-center items-center !h-100%" style="flex-direction: column;">
  198. <el-button @click="addCareer(index)" type="primary" class="cursor-pointer" :icon="Plus" circle />
  199. <el-button
  200. v-if="careerTrajectory.length > 1"
  201. class="mt-15px ml-0 cursor-pointer"
  202. @click="removeCareer(index)"
  203. type="danger"
  204. :icon="Delete"
  205. circle
  206. />
  207. </div>
  208. </el-col>
  209. </el-row>
  210. <el-row>
  211. <div class="m-title">地址信息</div>
  212. </el-row>
  213. <el-form-item label="中文地址" prop="address_zh">
  214. <el-input v-model="formQuery.address_zh" placeholder="请输入中文地址" />
  215. </el-form-item>
  216. <el-form-item label="英文地址" prop="address_en">
  217. <el-input v-model="formQuery.address_en" placeholder="请输入英文地址" />
  218. </el-form-item>
  219. <el-row :gutter="10">
  220. <el-col :span="12">
  221. <el-form-item label="邮政编码(中)" prop="postal_code_zh">
  222. <el-input v-model="formQuery.postal_code_zh" placeholder="请输入中文邮政编码" />
  223. </el-form-item>
  224. </el-col>
  225. <el-col :span="12">
  226. <el-form-item label="邮政编码(英)" prop="postal_code_en">
  227. <el-input v-model="formQuery.postal_code_en" placeholder="请输入英文邮政编码" />
  228. </el-form-item>
  229. </el-col>
  230. </el-row>
  231. <el-row v-if="formType === 'edit'">
  232. <div class="m-title">系统信息</div>
  233. </el-row>
  234. <el-form-item v-if="formType === 'edit'" label="状态">
  235. <el-tag v-if="itemData.status" :type="itemData.status === 'active' ? 'success' : 'danger'">
  236. {{ itemData.status === 'active' ? '已启用' : '已禁用' }}
  237. </el-tag>
  238. </el-form-item>
  239. <el-form-item label="创建时间" v-if="formType === 'edit'">
  240. <el-tag v-if="itemData.created_at" type="primary" effect="light">{{ itemData.created_at }}</el-tag>
  241. </el-form-item>
  242. <el-form-item label="更新时间" v-if="formType === 'edit'">
  243. <el-tag v-if="itemData.updated_at" type="primary" effect="light">{{ itemData.updated_at }}</el-tag>
  244. </el-form-item>
  245. </el-form>
  246. </div>
  247. </div>
  248. <template #footer>
  249. <el-button @click="handleSave" type="success" :disabled="analysisLoading">保 存</el-button>
  250. <el-button @click="showAnalysisTable = false">取 消</el-button>
  251. </template>
  252. </Dialog>
  253. </ContentWrap>
  254. <MergeForm ref="mergeFormRef" @refresh="getList" />
  255. </template>
  256. <script setup>
  257. import { dateFormatter } from '@/utils/formatTime'
  258. import { talentLabelingApi } from '@/api/menduner/system/talentMap/labeling'
  259. import { Delete, Plus } from '@element-plus/icons-vue'
  260. import MergeForm from '../../components/merge.vue'
  261. /** 人才地图 列表 */
  262. defineOptions({ name: 'TalentMapCard' })
  263. const message = useMessage() // 消息弹窗
  264. const { t } = useI18n() // 国际化
  265. const loading = ref(false) // 列表的加载中
  266. const list = ref([]) // 列表的数据
  267. const total = ref(0) // 列表的总页数
  268. const queryParams = reactive({
  269. name: undefined,
  270. })
  271. const queryFormRef = ref() // 搜索的表单
  272. /** 查询列表 */
  273. const getList = async () => {
  274. loading.value = true
  275. try {
  276. list.value = []
  277. const data = await talentLabelingApi.getCardList()
  278. list.value = data ? data.reverse() : []
  279. } finally {
  280. loading.value = false
  281. }
  282. }
  283. /** 搜索按钮操作 */
  284. const handleQuery = (type) => {
  285. // if (type === 'search') {
  286. // message.warning('搜索正在建设中...')
  287. // return
  288. // }
  289. getList()
  290. }
  291. /** 重置按钮操作 */
  292. const resetQuery = () => {
  293. queryFormRef.value.resetFields()
  294. handleQuery()
  295. }
  296. const dealData = (item) => {
  297. itemData.value = { ...item }
  298. careerTrajectory.value = item?.career_path ? JSON.parse(JSON.stringify(item.career_path)) : []
  299. if (!careerTrajectory.value?.length) careerTrajectory.value = [{ company_name: null, position: null, current_date: null }]
  300. Object.keys(formQuery.value).forEach(key => {
  301. formQuery.value[key] = item[key] || null
  302. })
  303. }
  304. /** 删除按钮操作 */
  305. const handleDelete = async (id) => {
  306. try {
  307. // 删除的二次确认
  308. await message.delConfirm()
  309. // 发起删除
  310. await talentLabelingApi.deleteBusinessCard(id)
  311. message.success(t('common.delSuccess'))
  312. // 刷新列表
  313. setTimeout(async () => {
  314. await getList()
  315. }, 0)
  316. } catch {}
  317. }
  318. /** 编辑 */
  319. const { push } = useRouter()
  320. const handleEdit = async (item) => {
  321. formType.value = 'edit'
  322. dealData(item)
  323. file.value = null
  324. filePath.value = null
  325. try {
  326. if (!item?.image_path) {
  327. showAnalysisTable.value = true
  328. return
  329. }
  330. const data = await talentLabelingApi.getBusinessCardImage(item.image_path)
  331. if (!data?.type) return
  332. file.value = new File([data], item.image_path, { type: data.type })
  333. filePath.value = URL.createObjectURL(data)
  334. } catch (error) {
  335. console.log('打印->getBusinessCardImage', error)
  336. } finally {
  337. showAnalysisTable.value = true
  338. }
  339. }
  340. /** 禁用按钮操作 */
  341. const handleDisable = async (item) => {
  342. if (!item?.id) return message.warning('操作失败,请稍后再试')
  343. try {
  344. // 禁用的二次确认
  345. const status = item.status === 'active' ? 'inactive' : 'active'
  346. const text = status === 'inactive' ? '禁用' : '启用'
  347. await message.delConfirm(`是否${text}该名片?`)
  348. // 发起禁用
  349. await talentLabelingApi.updateBusinessCardStatus({
  350. status,
  351. }, item.id)
  352. message.success(`${text}成功`)
  353. // 刷新列表
  354. await getList()
  355. } catch {}
  356. }
  357. const formQuery = ref({
  358. name_zh: undefined,
  359. name_en: undefined,
  360. title_zh: undefined,
  361. title_en: undefined,
  362. mobile: undefined,
  363. phone: undefined,
  364. email: undefined,
  365. hotel_zh: undefined,
  366. hotel_en: undefined,
  367. brand_zh: undefined,
  368. brand_en: undefined,
  369. affiliation_zh: undefined,
  370. affiliation_en: undefined,
  371. brand_group: undefined,
  372. address_zh: undefined,
  373. address_en: undefined,
  374. postal_code_zh: undefined,
  375. postal_code_en: undefined,
  376. birthday: undefined,
  377. residence: undefined,
  378. })
  379. const careerTrajectory = ref([{ company_name: null, position: null, current_date: null }])
  380. const file = ref(null)
  381. const uploadFile = ref(null)
  382. const uploadChange = (raw) => {
  383. file.value = raw
  384. }
  385. const addCareer = () => {
  386. careerTrajectory.value = careerTrajectory.value || []
  387. careerTrajectory.value.push({ company_name: null, position: null, current_date: null })
  388. }
  389. const removeCareer = (index) => {
  390. if (careerTrajectory.value.length <= 1) return
  391. careerTrajectory.value.splice(index, 1)
  392. }
  393. // 更新
  394. const showAnalysisTable = ref(false)
  395. const formLoading = ref(false)
  396. const formType = ref('')
  397. const formRef = ref() // 表单 Ref
  398. const mergeFormRef = ref() // 合并表单 Ref
  399. const handleSave = async () => {
  400. try {
  401. formLoading.value = true
  402. formQuery.value.career_path = careerTrajectory
  403. Object.assign(itemData.value, formQuery.value)
  404. let result = {}
  405. if (formType.value === 'create') {
  406. uploadFile.value.append('card_data', JSON.stringify(itemData.value))
  407. result = await talentLabelingApi.createBusinessCard(uploadFile.value)
  408. message.success('新增成功')
  409. if (result.code === 202 || result.message.includes('疑似重复')) {
  410. if (!result.data?.main_card?.id) return
  411. message.notifyWarning('发现与当前名片的疑似数据,请处理')
  412. mergeFormRef.value.open(result.data?.main_card?.id)
  413. }
  414. } else {
  415. await talentLabelingApi.updateBusinessCard(itemData.value, itemData.value.id)
  416. message.success('更新成功')
  417. }
  418. showAnalysisTable.value = false
  419. // 刷新列表
  420. getList()
  421. } catch (error) {
  422. console.log('更新失败', error)
  423. } finally {
  424. uploadFile.value = null
  425. formLoading.value = false
  426. }
  427. }
  428. // 关闭解析弹窗
  429. const handleCancel = () => {
  430. // console.log('关闭解析弹窗')
  431. openUploadImg.value = false
  432. analysisLoading.value = false
  433. }
  434. // 解析中
  435. const analysisLoading = ref(false)
  436. const filePath = ref(null)
  437. const itemData = ref({})
  438. const handleAnalysis = async () => {
  439. if (!filePath.value) {
  440. message.warning('请先上传名片!')
  441. return
  442. }
  443. try {
  444. analysisLoading.value = true
  445. // 开始解析
  446. uploadFile.value = new FormData()
  447. uploadFile.value.append('image', file.value)
  448. message.warning('正在解析...')
  449. const index = createAnalysisNum.value
  450. const res = await talentLabelingApi.businessCardParse(uploadFile.value)
  451. if (index !== createAnalysisNum.value || !openUploadImg.value) return // 不是最新的名片解析数据(用户在解析完成前已重新上传)或用户已取消解析
  452. dealData(res?.data || res)
  453. openUploadImg.value = false
  454. showAnalysisTable.value = true
  455. message.success('名片解析成功')
  456. } catch (error) {
  457. console.log('解析失败', error)
  458. uploadFile.value = null
  459. } finally {
  460. analysisLoading.value = false
  461. }
  462. }
  463. // 新增
  464. const openUploadImg = ref(false)
  465. const createAnalysisNum = ref(0)
  466. const handleAdd = () => {
  467. formType.value = 'create'
  468. file.value = null
  469. filePath.value = null
  470. analysisLoading.value = false
  471. createAnalysisNum.value++
  472. careerTrajectory.value = [{ company_name: null, position: null, current_date: null }]
  473. openUploadImg.value = true
  474. }
  475. /** 初始化 **/
  476. onMounted(() => {
  477. getList()
  478. })
  479. </script>
  480. <style lang="scss" scoped>
  481. .analysisInfoBox {
  482. display: flex;
  483. .image {
  484. width: 52%;
  485. max-height: 70vh;
  486. padding-right: 12px;
  487. overflow: auto;
  488. }
  489. .formBox {
  490. flex: 1;
  491. max-height: 70vh;
  492. padding: 12px;
  493. overflow: auto;
  494. background-color: #f5f7f9;
  495. .m-title {
  496. margin: 12px 8px;
  497. font-size: 13px;
  498. // font-weight: bold;
  499. color: #999;
  500. }
  501. .trajectoryBox {
  502. margin: 10px 20px;
  503. padding-top: 20px;
  504. padding-bottom: 5px;
  505. border-radius: 5px;
  506. background-color: #fff;
  507. }
  508. }
  509. }
  510. </style>