Browse Source

人才采集 解析方式合并

lifanagju_citu 1 day ago
parent
commit
bb5e0e8c3b

+ 6 - 8
src/views/menduner/system/talentMap/components/FormPage.vue

@@ -318,27 +318,25 @@ const setFormData = (data) => {
             : ['', '', ''];
         break;
       case 'career_path':
-        formQuery.value[key] = value || [];
+        formQuery.value[key] = careerTrajectory.value = value?.length ? cloneDeep(value) : [{ hotel_zh: null,  title_zh: null, date: null }];
         break;
       default:
         formQuery.value[key] = value ?? null;
     }
   });
-  careerTrajectory.value = cloneDeep(data.career_path ?? []);
 }
 
 const resetFormData = (data) => {
   formQuery.value = cloneDeep(defaultQuery)
 }
-
-// const getFormData = () => {
-//   return {
-//     ...formQuery.value
-//   }
-// }
+const changeLoading = (bool) => {
+  loading.value = bool
+}
 
 defineExpose({
 	setFormData,
+	resetFormData,
+	changeLoading,
 	formQuery
 })
 </script>

+ 144 - 0
src/views/menduner/system/talentMap/maintenance/gather/components/search.vue

@@ -0,0 +1,144 @@
+<template>
+    <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="姓名" prop="name">
+        <el-input v-model="queryParams.name" placeholder="请输入姓名" clearable @keyup.enter="handleQuery" class="!w-130px" />
+      </el-form-item>
+      <el-form-item label="联系电话" prop="phone">
+        <el-input v-model="queryParams.phone" placeholder="请输入联系电话" clearable @keyup.enter="handleQuery" class="!w-130px" />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery" type="primary"><Icon icon="ep:search" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <div v-if="!list?.length" class="tip">
+    <div>{{ tipContent }}</div>
+  </div>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true">
+      <!-- <el-table-column label="用户头像" align="center" prop="avatar" width="90px" fixed="left">
+        <template #default="scope">
+          <el-image v-if="scope.row.person?.avatar" class="h-80px w-80px" :src="scope.row.person?.avatar" lazy preview-teleported :preview-src-list="[scope.row.person?.avatar]" fit="contain" />
+        </template>
+      </el-table-column> -->
+      <el-table-column label="姓名" align="center" prop="person.name" fixed="left" />
+      <el-table-column label="联系电话" align="center" prop="user.phone" width="120px" />
+      <el-table-column label="求职状态" align="center" prop="person.jobStatus" width="130px">
+        <template #default="scope">
+          <dict-tag v-if="scope.row.person?.jobStatus" :type="DICT_TYPE.MENDUNER_JOB_SEEK_STATUS" :value="scope.row.person?.jobStatus" />
+        </template>
+      </el-table-column>
+      <el-table-column label="学历" align="center" prop="person.eduType">
+        <template #default="scope">
+          <dict-tag v-if="scope.row.person?.eduType" :type="DICT_TYPE.MENDUNER_EDUCATION_TYPE" :value="scope.row.person?.eduType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="工作经验" align="center" prop="person.expType">
+        <template #default="scope">
+          <dict-tag v-if="scope.row.person?.expType" :type="DICT_TYPE.MENDUNER_EXP_TYPE" :value="scope.row.person?.expType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" fixed="right" min-width="60">
+        <template #default="scope">
+          <el-button link type="primary" @click="handleDetail(scope.row)">{{ detailButTxt || '详情'}}</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      layout="total, prev, pager, next"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup>
+defineOptions({ name: 'TalentMapSearch' })
+import { TalentMap } from '@/api/menduner/system/talentMap'
+import { PersonInfoApi } from '@/api/menduner/system/person'
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+
+const emit = defineEmits(['detail'])
+const props = defineProps({
+  detailButTxt: String,
+  searchName: String
+})
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(false) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: '',
+  phone: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await PersonInfoApi.getPersonInfoPage(queryParams)
+    list.value = data?.list || []
+    total.value = data?.total || 0
+  } finally {
+    loading.value = false
+  }
+}
+
+const tipContent = '输入人才姓名、电话查询后可使用门墩儿人才库数据填充表单!'
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  if (!queryParams.name && !queryParams.phone) {
+    message.warning('请输入后查询 !')
+    return
+  }
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+const handleDetail = (row) => {
+  emit('detail', row.user.id)
+}
+
+/** 初始化 **/
+// onMounted(async () => {
+//   setTimeout(() => {
+//     if (props.searchName) {
+//       queryParams.name = props.searchName
+//     }
+//     getList()
+//   }, 1000)
+// })
+</script>
+
+<style lang="scss" scoped>
+.tip {
+  text-align: center;
+  margin-bottom: 10px;
+  color: #e6a23c;
+}
+</style>

+ 148 - 0
src/views/menduner/system/talentMap/maintenance/gather/components/webAnalysis.vue

@@ -0,0 +1,148 @@
+<template>
+	<ContentWrap>
+		<el-form
+			class="-mb-15px"
+			:model="queryParams"
+			ref="queryFormRef"
+			:inline="true"
+			label-width="90px"
+		>
+			<el-form-item label="url抓取数据" prop="urls">
+				<el-input
+					v-model="queryParams.urls"
+					class="!w-60vw"
+					type="textarea"
+					:rows="1"
+					placeholder="请输入需要爬取的页面,多个页面请用 ',' 隔开"
+				/>
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" plain :loading="loading" @click="handleExecute">执行</el-button>
+			</el-form-item>
+		</el-form>
+	</ContentWrap>
+
+	<ContentWrap v-if="contents.length">
+		<el-row gutter="20">
+			<el-col v-for="(content, index) in contents" :key="index" :span="12">
+				<el-card class="!h-500px" v-loading="!content.data">
+					<template #header>
+						<div class="flex items-center justify-between">
+							<el-text class="flex-1" truncated>{{ content.url }}</el-text>
+							<div class="!w-85px">
+								<Icon icon="ep:view" size="25" class="ml-10px cursor-pointer" color="#409eff" @click="showPage(content)" />
+								<Icon icon="ep:refresh" size="25" class=" ml-18px cursor-pointer" color="#409eff" @click="handleReload(content)" />
+							</div>
+						</div>
+					</template>
+					<div v-if="content.data">
+						<template v-if="typeof content.data === 'string'">{{ content.data }}</template>
+            <el-tabs v-else v-model="content.tab">
+              <el-tab-pane v-for="(v, k) in content.data.data[0]" :key="k" :label="k" :name="k" class="overflow-y-auto !h-360px">
+								<template v-if="k === 'html'">
+									<div class="position-sticky float-right">
+										<el-button
+											type="primary"
+											class="cursor-pointer"
+											@click="content.showHtml = !content.showHtml"
+											:icon="SetUp"
+											circle
+										/>
+									</div>
+                  <pre v-if="!content.showHtml">{{ v }}</pre>
+                  <div v-else v-html="v"></div>
+                </template>
+                <pre v-else>{{ v || '暂无数据' }}</pre>
+							</el-tab-pane>
+            </el-tabs>
+          </div>
+				</el-card>
+			</el-col>
+		</el-row>
+	</ContentWrap>
+
+	<el-drawer
+		v-model="drawer"
+		class="!w-50vw"
+		:with-header="false"
+		:modal="true"
+	>
+		<iframe class="!w-100% !h-[calc(100vh-90px)]" :src="drawerUrl" frameborder="0"></iframe>
+		<el-divider class="!ma-0" />
+		<div class="position-sticky left-20px !h-50px lh-50px">
+			<el-button type="primary" class="!w-100px" @click="drawer = false; drawerUrl = ''">关 闭</el-button>
+		</div>
+	</el-drawer>
+</template>
+
+<script setup>
+/** 人才采集 网页解析 */
+import FirecrawlApp from '@mendable/firecrawl-js'
+import { SetUp } from '@element-plus/icons-vue'
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(false)
+const queryParams = reactive({
+	urls: 'https://mp.weixin.qq.com/s/WeCRR3zN3fPvlGR4t8YFDA'
+})
+const queryFormRef = ref()
+const contents = ref([])
+const drawer = ref(false)
+const drawerUrl = ref('')
+
+const showPage = (content) => {
+	drawer.value = true
+	drawerUrl.value = content.url
+}
+
+const handleReload = async (content) => {
+	content.data = null
+	const res = await handleData(queryParams.urls)
+	content.tab = 0
+	content.data = res
+}
+
+const handleData = async (url) => {
+	try {
+    const app = new FirecrawlApp({ apiKey: 'fc-85c1550c6db64ce4ae8f2d2cd2606e6f' })
+    const crawlResponse = await app.crawlUrl(url, {
+      limit: 100,
+      scrapeOptions: {
+        formats: ['markdown', 'html']
+      }
+    })
+    if (!crawlResponse.success) {
+      throw new Error(`Failed to crawl: ${crawlResponse.error}`)
+    }
+    return crawlResponse
+  } catch (error) {
+    return error.message
+  }
+}
+
+// 执行
+const handleExecute = async () => {
+	if (!queryParams.urls) return
+	contents.value = []
+	const urls = queryParams.urls.split(',').map(url => url.trim()).filter(url => url)
+	if (urls.length === 0) return
+
+	urls.forEach(url => {
+	  contents.value.push({ url, tab: 'markdown', showHtml: false, data: null })
+	})
+
+	const crawlPromises = urls.map(async (url, index) => {
+		const res = await handleData(url)
+		contents.value[index] = { ...contents.value[index], data: res }
+	})
+
+	try {
+		await Promise.all(crawlPromises)
+		console.log('All crawls completed:', contents.value); // 可在此处添加成功回调
+	} catch (error) {
+		console.error('爬取过程中发生错误:', error);
+	}
+}
+</script>

+ 579 - 18
src/views/menduner/system/talentMap/maintenance/gather/index.vue

@@ -1,24 +1,585 @@
 <template>
-  <div>
-		<el-tabs v-model="activeName" type="border-card">
-			<el-tab-pane label="简历解析" name="resume">
-				<resumePage/>
-			</el-tab-pane>
-			<el-tab-pane label="名片解析" name="card">
-				<cardPage/>
-			</el-tab-pane>
-			<el-tab-pane label="网页解析" name="webpage">
-				<webPageParsing/>
-			</el-tab-pane>
-		</el-tabs>
-	</div>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="名称" prop="name">
+        <el-input v-model="queryParams.name" placeholder="请输入名称" clearable @keyup.enter="handleQuery" class="!w-180px" />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery('search')"><Icon icon="ep:search" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button type="primary" plain @click="handleAdd">
+          <Icon icon="ep:plus" class="mr-5px" /> 新增人才
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true">
+      <el-table-column label="中文名称" align="center" prop="name_zh" fixed="left" />
+      <el-table-column label="英文名称" align="center" prop="name_en" />
+      <el-table-column label="职位" align="center" prop="title_zh" />
+      <el-table-column label="酒店/公司" align="center" prop="hotel_zh" />
+      <el-table-column label="手机号码" align="center" prop="mobile" />
+      <el-table-column label="固定电话" align="center" prop="phone" />
+      <el-table-column label="状态" align="center" prop="status" width="80">
+        <template #default="scope">
+          <el-tag type="success" v-if="scope.row.status === 'active'">已启用</el-tag>
+          <el-tag type="danger" v-else>已禁用</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建日期" align="center" prop="created_at" :formatter="dateFormatter" />
+      <el-table-column label="操作" align="center" fixed="right" min-width="110">
+        <template #default="scope">
+          <el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
+          <el-button link type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
+          <el-button link :type="scope.row.status === 'active' ? 'warning': 'success'" @click="handleDisable(scope.row)">
+            {{ scope.row.status === 'active' ? '禁用' : '启用'}}
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 选择来源 -->
+    <Dialog title="解析方式" v-model="openSelect" width="500" @close="openSelect = false">
+			<el-radio-group v-model="radioValue" size="large" class="radioBox">
+				<el-radio
+					v-for="item in radioList"
+					:key="item.value"
+					:value="item.value"
+				>
+					{{ item.label }}
+				</el-radio>
+			</el-radio-group>
+      <template #footer>
+        <el-button type="primary" @click="handleSelect">确 认</el-button>
+        <el-button @click="null">取 消</el-button>
+      </template>
+    </Dialog>
+
+    <!-- 解析文件上传 -->
+    <Dialog :title="radioObject[radioValue]" v-model="dialog_upload" :width="DialogWidth" @close="handleCancel">
+      <div>
+        <!-- 门墩儿人才库 -->
+        <!-- <template v-if="radioValue === 'menduner'">
+        </template> -->
+        <!-- 简历解析 -->
+        <template v-if="radioValue === 'file'">
+          <el-upload
+            ref="uploadRef"
+            v-model:file-list="fileList"
+            :action="uploadUrl"
+            :auto-upload="false"
+            :data="data"
+            :limit="1"
+            :on-change="handleChange"
+            :on-error="submitFormError"
+            :on-exceed="handleExceed"
+            :on-success="submitFormSuccess"
+            :http-request="httpRequest"
+            accept=".pdf, doc, .docx"
+            drag
+            class="flex-1"
+          >
+            <i class="el-icon-upload"></i>
+            <div class="el-upload__text">上传附件, 将文件拖到此处,或 <em>点击上传</em></div>
+            <template #tip>
+              <div class="el-upload__tip color-red">
+                提示:仅允许导入 pdf、doc、docx 格式文件!
+              </div>
+            </template>
+          </el-upload>
+        </template>
+        <!-- 名片解析 -->
+        <template v-if="radioValue === 'card'">
+          <UploadImg
+            v-model="cardImgUrl"
+            :limit="1"
+            :uploadSuccessTip="false"
+            @handle-change="cardUploadChange"
+            height="150px" width="150px" style="margin: 20px auto; width: 150px;"
+          >
+            <template #tip>{{ cardImgUrl ? '' : '请上传名片' }}</template>
+          </UploadImg>
+        </template>
+        <!-- 网页解析 -->
+        <!-- <template v-if="radioValue === 'web'"></template> -->
+      </div>
+      <template #footer>
+        <el-button @click="handleAnalysis" type="success" :disabled="analysisLoading" :loading="analysisLoading">解 析</el-button>
+        <el-button @click="handleCancel">取 消</el-button>
+      </template>
+    </Dialog>
+
+    <!-- 解析回显 -->
+    <Dialog :title="radioObject[radioValue]" v-model="dialog_analysisInfo" width="80%">
+      <div class="analysisInfoBox">
+        <div class="analysisFile">
+          <!-- 门墩儿人才库 -->
+          <template v-if="radioValue === 'menduner'">
+            <Search @detail="handleDetail" detailButTxt="应用" :searchName="formData?.name_zh || formData?.name_en || ''" />
+          </template>
+          <!-- 简历解析 -->
+          <template v-if="radioValue === 'file'">
+            <div v-if="fileUrl" style="position: relative;">
+              <div class="text-right m-b-10px">
+                <el-button v-if="!isEdit" @click="handleText">查看文本信息</el-button>
+                <el-button type="primary" @click="handleResetUpload">重新上传简历</el-button>
+              </div>
+              <IFrame :src="fileUrl" />
+              <el-drawer
+                v-model="drawer"
+                modal-class="drawer"
+                size="75%"
+                direction="ltr"
+                title="简历解析(可复制文本使用)"
+              >
+                <p v-for="(text, index) in resumeTxt" :key="text + index">{{ text }}</p>
+              </el-drawer>
+            </div>
+            <el-upload
+              v-else
+              ref="uploadRef"
+              v-model:file-list="fileList"
+              :action="uploadUrl"
+              :auto-upload="false"
+              :data="data"
+              :limit="1"
+              :on-change="handleChange"
+              :on-error="submitFormError"
+              :on-exceed="handleExceed"
+              :on-success="submitFormSuccess"
+              :http-request="httpRequest"
+              accept=".pdf, doc, .docx"
+              drag
+              class="flex-1"
+            >
+              <i class="el-icon-upload"></i>
+              <div class="el-upload__text">上传附件, 将文件拖到此处,或 <em>点击上传</em></div>
+              <template #tip>
+                <div class="el-upload__tip color-red">
+                  提示:仅允许导入 pdf、doc、docx 格式文件!
+                </div>
+              </template>
+            </el-upload>
+          </template>
+          <!-- 名片解析 -->
+          <template v-if="radioValue === 'card'">
+            <div class="image">
+              <el-image v-if="cardImgUrl" class="!w-100%" :src="cardImgUrl" />
+              <div v-else>
+                <UploadImg
+                  v-model="cardImgUrl"
+                  :limit="1"
+                  :uploadSuccessTip="false"
+                  drag
+                  buttonUpload
+                  @handle-change="cardUploadChange"
+                  height="32px" width="104px"
+                  style="margin: 0 auto; width: 104px;margin-top: 40%;"
+                >
+                  <template #tip>{{ cardImgUrl ? '' : '请上传名片' }}</template>
+                </UploadImg>
+              </div>
+            </div>
+          </template>
+          <!-- 网页解析 -->
+          <template v-if="radioValue === 'web'">
+            <webAnalysis v-if="showWebAnalysis"/>
+          </template>
+        </div>
+        <FormPage ref="FormPageRef" :analysisType="analysisType" :itemData="formData" />
+      </div>
+      <template #footer>
+        <el-button @click="handleSave" type="success" :disabled="analysisLoading">保 存</el-button>
+        <el-button @click="dialog_analysisInfo = false">取 消</el-button>
+      </template>
+    </Dialog>
+  </ContentWrap>
+
+  <MergeForm ref="mergeFormRef" @refresh="getList" />
 </template>
 
 <script setup>
 defineOptions({ name: 'TalentMapStoreIndex' })
-import resumePage from './resume/index.vue'
-import cardPage from './businessCard/index.vue'
-import webPageParsing from './webPageParsing/index.vue'
+import { dateFormatter } from '@/utils/formatTime'
+import { talentLabelingApi } from '@/api/menduner/system/talentMap/labeling'
+import { TalentMap } from '@/api/menduner/system/talentMap'
+import { Delete, Plus } from '@element-plus/icons-vue'
+import MergeForm from '@/views/menduner/system/talentMap/components/merge.vue'
+import FormPage from '@/views/menduner/system/talentMap/components/FormPage.vue'
+// import uploadDialog from './components/uploadDialog.vue'
+import { timesTampChange, timestampToAge } from '@/utils/transform/date'
+import Search from './components/search.vue'
+import webAnalysis from './components/webAnalysis.vue'
+import { useUpload } from '@/components/UploadFile/src/useUpload'
+import { commonApi } from '@/api/menduner/common'
+import { Base64 } from 'js-base64'
+
+const baseUrl = import.meta.env.VITE_PREVIEW_URL
+const { uploadUrl, httpRequest } = useUpload()
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(false) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  name: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+const dialog_upload = ref(false)
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    list.value = []
+    const data = await talentLabelingApi.getCardList()
+    list.value = data ? data.reverse() : []
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = (type) => {
+  if (type !== 'reset') {
+    message.warning('搜索正在建设中...')
+    return
+  }
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery('reset')
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await talentLabelingApi.deleteBusinessCard(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    setTimeout(async () => {
+      await getList()
+    }, 0)
+  } catch {}
+}
+
+/** 编辑 */
+const { push } = useRouter()
+const handleEdit = async (item) => {
+  analysisType.value = 'edit'
+  // dealData(item)
+  formData.value = item
+  cardUploadRow.value = null
+  cardImgUrl.value = null
+  try {
+    if (!item?.image_path) {
+      dialog_analysisInfo.value = true
+      return
+    }
+    const data = await talentLabelingApi.getBusinessCardImage(item.image_path)
+    if (!data?.type) return
+
+    cardUploadRow.value = new File([data], item.image_path, { type: data.type })
+    cardImgUrl.value = URL.createObjectURL(data)
+  } catch (error) {
+    console.log('打印->getBusinessCardImage', error)
+  } finally {
+    dialog_analysisInfo.value = true
+  }
+}
+
+/** 禁用按钮操作 */
+const handleDisable = async (item) => {
+  if (!item?.id) return message.warning('操作失败,请稍后再试')
+  try {
+    // 禁用的二次确认
+    const status = item.status === 'active' ? 'inactive' : 'active'
+    const text = status === 'inactive' ? '禁用' : '启用'
+    
+    await message.delConfirm(`是否${text}该名片?`)
+    // 发起禁用
+    await talentLabelingApi.updateBusinessCardStatus({
+      status,
+    }, item.id)
+    message.success(`${text}成功`)
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+// 更新
+const dialog_analysisInfo = ref(false)
+const formLoading = ref(false)
+const analysisType = ref('')
+const FormPageRef = ref(null)
+const mergeFormRef = ref() // 合并表单 Ref
+const handleSave = async () => {
+  const params = { ...FormPageRef.value.formQuery }
+  if (!params.name_zh) return message.warning('请填写姓名!')
+  
+  // 数组转为字符串保存
+  if (Array.isArray(params?.mobile)) {
+    params.mobile = params.mobile.filter(i => Boolean(i)).map(j => String(j).replace(/,|,/g, '')).join(',');
+  }
+
+  console.log(params, 'handleSubmit')
+  try {
+    formLoading.value = true
+    let result = {}
+
+    if (analysisType.value === 'create') {
+      if (cardFileQuery.value) {
+        cardFileQuery.value.append('card_data', JSON.stringify(params)) // 名片
+        result = await talentLabelingApi.createBusinessCard(cardFileQuery.value)
+      } else {
+        // 结构化数据源 不传递文件
+        result = await talentLabelingApi.createBusinessCardPost(params)
+      }
+      message.success('新增成功')
+      dialog_analysisInfo.value = false
+      // 刷新列表
+      getList()
+
+      if (result.code === 202 || result.message.includes('疑似重复')) {
+        if (!result.data?.main_card?.id) return
+        
+        await message.confirm('发现与当前名片的疑似重复数据,去处理')
+        mergeFormRef.value.open(result.data?.main_card?.id)
+      }
+    } else {
+      await talentLabelingApi.updateBusinessCard(params, formData.value.id)
+      message.success('更新成功')
+      dialog_analysisInfo.value = false
+      // 刷新列表
+      getList()
+    }
+  } catch (error) {
+    console.log('更新失败', error)
+  } finally {
+    cardFileQuery.value = null
+    formLoading.value = false
+  }
+}
+
+// 解析中
+const analysisLoading = ref(false)
+const formData = ref({})
+const handleAnalysis = async () => {
+  // 开始解析
+  analysisLoading.value = true
+  cardFileQuery.value = null
+	const type = radioValue.value
+  try {
+    // if (type === 'menduner') { // 门墩儿人才库
+    // } else 
+    if (type === 'file') { // 简历解析
+      if (!fileUrl.value) return message.warning('获取文件失败,请重新上传!')
+      const data = await commonApi.resumeParser({ fileUrl: fileUrl.value })
+      setItemDataValue(data)
+    } else if (type === 'card') { // 名片解析
+      if (!cardImgUrl.value) {
+        message.warning('请先上传名片!')
+        return
+      }
+      cardFileQuery.value = new FormData()
+      cardFileQuery.value.append('image', cardUploadRow.value)
+      message.warning('正在解析...')
+  
+      const index = createAnalysisNum.value
+      const res = await talentLabelingApi.businessCardParse(cardFileQuery.value)
+      if (index !== createAnalysisNum.value || !dialog_upload.value) return // 不是最新的名片解析数据(用户在解析完成前已重新上传)或用户已取消解析
+      formData.value = res?.data || res
+      message.success('名片解析成功')
+    }
+    // else if (type === 'web') {}
+
+    dialog_upload.value = false
+    dialog_analysisInfo.value = true
+  } catch (error) {
+    console.log('解析失败', error)
+    cardFileQuery.value = null
+  } finally {
+    analysisLoading.value = false
+  }
+}
+
+// 简历解析
+const fileUrl = ref('') // https://minio.menduner.com/dev/person/229988673960153088/attachment/ee3eb21f45e13ede3557a03d18585ed80c5b4212ac5634e3436e309afaa8fe6a.pdf
+const uploadRef = ref()
+const fileList = ref([])
+const data = ref({ path: '' })
+// 文件上传
+const handleChange = async (file) => {
+  data.value.path = file.name
+  unref(uploadRef)?.submit()
+  if (!fileList.value.length) return
+
+  const url = fileList.value[0].response.data
+  fileUrl.value = !url.includes('.pdf') ?  `${baseUrl}/onlinePreview?url=${encodeURIComponent(Base64.encode(url))}` : url
+  if (dialog_analysisInfo.value) {
+    if (FormPageRef.value?.changeLoading) FormPageRef.value.changeLoading(true)
+    message.warning('正在解析...')
+    const data = await commonApi.resumeParser({ fileUrl: fileUrl.value })
+    setItemDataValue(data)
+    if (FormPageRef.value?.changeLoading) FormPageRef.value.changeLoading(false)
+  }
+}
+const submitFormError = () => {
+  message.error('上传失败,请您重新上传!')
+}
+const handleExceed = () => {
+  message.error('最多只能上传一个文件!')
+}
+const submitFormSuccess = (e) => {
+  // 清理
+  // unref(uploadRef)?.clearFiles()
+}
+
+const drawer = ref(false)
+const resumeTxt = ref([])
+// 查看文本信息
+const handleText = () => {
+  drawer.value = true
+}
+// 重新上传简历
+const handleResetUpload = async () => {
+  await message.confirm('是否确定重新上传简历?确定后将清空当前信息')
+  fileUrl.value = ''
+  data.value.path = ''
+  fileList.value = []
+  setItemDataValue('reset')
+}
+
+// 名片解析 
+const createAnalysisNum = ref(0)
+const cardFileQuery = ref(null)
+const cardUploadRow = ref(null)
+const cardImgUrl = ref(null)
+const cardUploadChange = (raw) => {
+  cardUploadRow.value = raw
+}
+
+// 搜索-查看详情
+const handleDetail = async (userId) => {
+  if (!userId) return message.warning('请先选择人才!')
+  try {
+    const data = await TalentMap.getTalentMapDetail(userId)
+    // 去除id
+    setItemDataValue(data)
+  } catch {}
+}
+
+const setItemDataValue = (data) => {
+  if (data === 'reset') {
+    resumeTxt.value = ''
+    if (FormPageRef.value?.resetFormData) FormPageRef.value.resetFormData()
+  }
+  formData.value = {
+    name_zh: data?.person?.name || '',
+    email: data?.person?.email || '',
+    mobile: data?.person?.phone || '',
+    birthday: data?.person?.birthday ? timesTampChange(data.person.birthday, 'Y-M-D') : '',
+    age: data?.person?.birthday ? timestampToAge(data.person.birthday) : null,
+    career_path: data?.workList ? data.workList.map(e => {
+      return {
+        hotel_zh: e?.enterpriseName || null,
+        title_zh: e?.positionName || null,
+        date: e?.startTime ? timesTampChange(e.startTime, 'Y-M-D') : null
+      }
+    }) : null,
+    created_at: data?.person?.createTime ? timesTampChange(data.person.createTime, 'Y-M-D') : null,
+    updated_at: data?.person?.updateTime ? timesTampChange(data.person.updateTime, 'Y-M-D') : null,
+  }
+  resumeTxt.value = data?.resume?.rawText?.split('\n') || ''
+  if (FormPageRef.value?.setFormData) FormPageRef.value.setFormData(formData.value)
+}
+
+const DialogWidth = ref('500')
+const showWebAnalysis = ref(false)
+// 选择解析方式
+const handleSelect = () => {
+  openSelect.value = false
+  showWebAnalysis.value = false
+	const type = radioValue.value
+  if (type === 'card') {
+    createAnalysisNum.value++
+  }
+  if (type === 'menduner') {
+    dialog_analysisInfo.value = true
+    return
+  }
+  if (type === 'web') {
+    showWebAnalysis.value = true
+    dialog_analysisInfo.value = true
+    return
+  }
+  dialog_upload.value = true
+}
+
+// 关闭上传弹窗
+const handleCancel = () => {
+  dialog_upload.value = false
+  analysisLoading.value = false
+}
+
+const openSelect = ref(false)
+const radioObject = { card: '名片解析', file: '简历解析', web: '网页解析', menduner: '门墩儿人才库(普通新增)' }
+const radioList = ref(Object.keys(radioObject).map(key => ({ value: key, label: radioObject[key]}) ))
+const defaultValue = radioList.value[0].value // 默认选中
+const radioValue = ref(defaultValue)
+// 新增解析
+const handleAdd = () => {
+  cardUploadRow.value = null
+  cardImgUrl.value = null
+  analysisLoading.value = false
+  analysisType.value = 'create'
+  radioValue.value = defaultValue // 重置解析类型
+	// 
+  openSelect.value = true
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+
+</script>
+
+<style lang="scss" scoped>
+.analysisInfoBox {
+  display: flex;
+  .analysisFile {
+    width: 50%;
+    max-height: 70vh;
+    padding-right: 12px;
+    overflow: auto;
+  }
+}
+.radioBox {
+	margin: 40px 0;
+}
 
-const activeName = ref('resume')
-</script>
+:deep(.drawer) {
+  position: absolute;
+  .el-drawer {
+    background-color: aliceblue;
+  }
+}
+</style>