Xiao_123 пре 2 месеци
родитељ
комит
ff7d66e051

+ 13 - 0
src/api/menduner/system/talentMap/examine.ts

@@ -0,0 +1,13 @@
+import request from '@/config/axios'
+
+// 核验入库
+export const talentExamineApi = {
+	// 列表
+	getExamineList: async (params: any) => {
+		return await request.get({ 
+			url: `/api/parse/get-parsed-talents`,
+			params,
+			baseURL: import.meta.env.VITE_BASE_URL
+		})
+	}
+}

+ 67 - 35
src/views/menduner/system/talentMap/components/info.vue

@@ -2,43 +2,58 @@
   <div class="!h-65vh overflow-y-auto">
     <slot name="header"></slot>
     <el-descriptions title="基础信息" :column="2" border>
-      <el-descriptions-item min-width="120" label="姓名(中)">{{ data?.name_zh || '--' }}</el-descriptions-item>
-      <el-descriptions-item min-width="120" label="姓名(英)">{{ data?.name_en || '--' }}</el-descriptions-item>
-      <el-descriptions-item min-width="120" label="职位/头衔(中)">{{ data?.title_zh || '--' }}</el-descriptions-item>
-      <el-descriptions-item min-width="120" label="职位/头衔(英)">{{ data?.title_en || '--' }}</el-descriptions-item>
-      <el-descriptions-item min-width="120" label="生日">{{ data?.birthday || '--' }}</el-descriptions-item>
-      <el-descriptions-item min-width="120" label="居住地">{{ data?.residence || '--' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="姓名(中)">{{ data?.name_zh || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="姓名(英)">{{ data?.name_en || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="职位/头衔(中)">{{ data?.title_zh || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="职位/头衔(英)">{{ data?.title_en || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="生日">{{ data?.birthday || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="籍贯">{{ data?.native_place || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="年龄">{{ data?.age || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="手机号码">{{ data?.phone || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="固定电话">{{ data?.mobile || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="电子邮箱">{{ data?.email || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="中文地址">{{ data?.address_zh || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="英文地址">{{ data?.address_en || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="邮政编码">{{ data?.postal_code_zh || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="工作地">{{ data?.residence || '' }}</el-descriptions-item>
+      <!-- <el-descriptions-item min-width="120" label="邮政编码(英)">{{ data?.postal_code_en || '--' }}</el-descriptions-item> -->
     </el-descriptions>
-    <el-descriptions title="联系方式" class="mt-20px" :column="2" border>
+
+    <!-- <el-descriptions title="联系方式" class="mt-20px" :column="2" border>
       <el-descriptions-item min-width="120" label="手机号码">{{ data?.phone || '--' }}</el-descriptions-item>
       <el-descriptions-item min-width="120" label="固定电话">{{ data?.mobile || '--' }}</el-descriptions-item>
       <el-descriptions-item min-width="120" label="电子邮箱">{{ data?.email || '--' }}</el-descriptions-item>
-    </el-descriptions>
+    </el-descriptions> -->
+
     <el-descriptions title="酒店/公司信息" class="mt-20px" :column="2" border>
-      <el-descriptions-item min-width="120" label="酒店/公司名称(中)">{{ data?.hotel_zh || '--' }}</el-descriptions-item>
-      <el-descriptions-item min-width="120" label="酒店/公司名称(英)">{{ data?.hotel_en || '--' }}</el-descriptions-item>
-      <el-descriptions-item min-width="120" label="隶属关系(中)">{{ data?.affiliation_zh || '--' }}</el-descriptions-item>
-      <el-descriptions-item min-width="120" label="隶属关系(英)">{{ data?.affiliation_en || '--' }}</el-descriptions-item>
-      <el-descriptions-item min-width="120" label="品牌名称(中)">{{ data?.brand_zh || '--' }}</el-descriptions-item>
-      <el-descriptions-item min-width="120" label="品牌名称(英)">{{ data?.brand_en || '--' }}</el-descriptions-item>
-      <el-descriptions-item min-width="120" label="品牌组合">{{ data?.brand_group || '--' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="酒店/公司名称(中)">{{ data?.hotel_zh || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="酒店/公司名称(英)">{{ data?.hotel_en || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="隶属关系(中)">{{ data?.affiliation_zh || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="隶属关系(英)">{{ data?.affiliation_en || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="品牌名称(中)">{{ data?.brand_zh || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="品牌名称(英)">{{ data?.brand_en || '' }}</el-descriptions-item>
+      <el-descriptions-item min-width="120" label="品牌组合">{{ data?.brand_group || '' }}</el-descriptions-item>
     </el-descriptions>
+
     <el-descriptions v-if="data?.career_path && data.career_path.length > 0" title="职业轨迹" class="mt-20px" border />
-    <el-timeline class="ml-12px" v-if="data?.career_path && data.career_path.length > 0">
-      <el-timeline-item color="#0bbd87" center v-for="(val, index) in data.career_path" :key="index">
-        <el-descriptions title="" border>
-          <el-descriptions-item min-width="120" label="酒店名称">{{ val.hotel_zh || '--' }}</el-descriptions-item>
-          <el-descriptions-item min-width="120" label="职位名称">{{ val.title_zh || '--' }}</el-descriptions-item>
-          <el-descriptions-item min-width="120" label="任职时间">{{ val.date || '--' }}</el-descriptions-item>
-        </el-descriptions>
+    <el-timeline v-if="data?.career_path && data.career_path.length > 0" class="pl-20px">
+      <el-timeline-item center placement="top" color="#0bbd87" v-for="(val, index) in data.career_path" :key="index">
+        <div class="timeline-item">
+          <div class="timeline-item-time">{{ val.date || '未填写任职时间' }}</div>
+          <div class="timeline-item-content">
+            <div class="timeline-item-name">{{ val.hotel_zh || '未填写酒店名称' }}</div>
+            <div class="timeline-item-name">{{ val.title_zh || '未填写职位名称' }}</div>
+          </div>
+        </div>
       </el-timeline-item>
     </el-timeline>
-    <el-descriptions title="地址信息" class="mt-20px" :column="2" border>
+
+    <!-- <el-descriptions title="地址信息" class="mt-20px" :column="2" border>
       <el-descriptions-item min-width="120" label="中文地址">{{ data?.address_zh || '--' }}</el-descriptions-item>
       <el-descriptions-item min-width="120" label="英文地址">{{ data?.address_en || '--' }}</el-descriptions-item>
       <el-descriptions-item min-width="120" label="邮政编码(中)">{{ data?.postal_code_zh || '--' }}</el-descriptions-item>
       <el-descriptions-item min-width="120" label="邮政编码(英)">{{ data?.postal_code_en || '--' }}</el-descriptions-item>
-    </el-descriptions>
+    </el-descriptions> -->
     <!-- <el-descriptions title="人才标签" class="mt-20px" :column="2" border />
     <el-tag v-for="k in talentTags" :key="k.talent" type="success" class="mr-10px my-10px">{{ k.tag }}</el-tag> -->
     <slot name="thumbnail"></slot>
@@ -47,20 +62,37 @@
 
 <script setup>
 defineOptions({ name: 'TalentMapStoreMergeInfo' })
-const props = defineProps({
+defineProps({
   data: {
     type: Object,
     default: () => {}
   }
 })
+</script>
 
-// const talentTags = ref([]) 
-
-// watch(
-//   () => props.data,
-//   () => {
-//     talentTags.value = props.data?.talentTags || []
-//   },
-//   { immediate: true }
-// )
-</script>
+<style scoped lang="scss">
+.timeline-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  color: var(--color-666);
+  font-size: 13px;
+  .timeline-item-time {
+    width: 20%;
+    min-width: 200px;
+  }
+  .timeline-item-content {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    .timeline-item-name {
+      width: 50%;
+      padding-left: 12px;
+    }
+  }
+}
+.el-timeline-item {
+  padding-bottom: 0;
+}
+</style>

+ 40 - 25
src/views/menduner/system/talentMap/maintenance/examine/index.vue

@@ -8,7 +8,7 @@
       :inline="true"
       label-width="40px"
     >
-      <el-form-item label="来源" prop="task_type">
+      <!-- <el-form-item label="来源" prop="task_type">
         <el-select
           v-model="queryParams.task_type"
           placeholder="请选择来源"
@@ -18,6 +18,17 @@
         >
           <el-option v-for="(val, index) in taskType" :label="val.label" :value="val.value" :key="index" />
         </el-select>
+      </el-form-item> -->
+      <el-form-item label="状态" prop="task_status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择状态"
+          clearable
+          class="!w-200px"
+          @change="handleQuery"
+        >
+          <el-option v-for="(val, index) in ['待审核', '已入库', '已拒绝']" :label="val" :value="val" :key="index" />
+        </el-select>
       </el-form-item>
       <el-form-item>
         <el-button @click="handleQuery"><Icon icon="ep:search" /> 搜索</el-button>
@@ -29,11 +40,14 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true">
-      <el-table-column label="姓名" align="center" prop="" />
-      <el-table-column label="职位" align="center" prop="" />
-      <el-table-column label="酒店" align="center" prop="" />
-      <el-table-column label="联系电话" align="center" prop="" :formatter="dateFormatter" />
-      <el-table-column label="邮箱" align="center" prop="" :formatter="dateFormatter" />
+      <el-table-column label="姓名" align="center" prop="name_zh" />
+      <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="task_type" />
+      <el-table-column label="状态" align="center" prop="status" />
+      <el-table-column label="创建时间" align="center" prop="created_at" :formatter="dateFormatter" />
+      <el-table-column label="更新时间" align="center" prop="updated_at" :formatter="dateFormatter" />
       <el-table-column label="操作" align="center" fixed="right" min-width="110">
         <template #default="scope">
           <el-button
@@ -60,8 +74,8 @@
 <script setup>
 defineOptions({ name: 'TalentMapMaintenanceExamineIndex' })
 import { dateFormatter } from '@/utils/formatTime'
-import StorePage from '@/views/menduner/system/talentMap/maintenance/gather/components/store.vue'
-import { talentGatherApi } from '@/api/menduner/system/talentMap/gather'
+import StorePage from '@/views/menduner/system/talentMap/maintenance/examine/store.vue'
+import { talentExamineApi } from '@/api/menduner/system/talentMap/examine'
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
@@ -70,9 +84,10 @@ const loading = ref(false) // 列表的加载中
 const list = ref([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const queryParams = reactive({
-  page: 1,
-  per_page: 10,
-  task_status: undefined // 来源
+  // page: 1,
+  // per_page: 10,
+  // task_status: undefined // 来源
+  status: null
 })
 const queryFormRef = ref() // 搜索的表单
 const taskType = [
@@ -88,9 +103,10 @@ const getList = async () => {
   loading.value = true
   try {
     list.value = []
-    const data = await talentGatherApi.getTaskList(queryParams)
-    list.value = data.tasks ?? []
-    total.value = data.pagination.total ?? 0
+    const data = await talentExamineApi.getExamineList(queryParams)
+    list.value = data || []
+    // list.value = data.tasks ?? []
+    // total.value = data.pagination.total ?? 0
   } finally {
     loading.value = false
   }
@@ -98,7 +114,7 @@ const getList = async () => {
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
-  queryParams.page = 1
+  // queryParams.page = 1
   getList()
 }
 
@@ -110,17 +126,16 @@ const resetQuery = () => {
 
 // 入库
 const StorePageRef = ref(null)
-const handleStore = ({ task_type, parse_result, id, task_source }, isDetail = false) => {
-  const txt = isDetail ? '暂无详情查看' : '暂无解析数据可入库'
-  const isRecruitment = Boolean(task_type === '招聘')
-  if (isRecruitment) {
-    if (!task_source?.data) return message.warning(txt)
-  } else {
-    if (!parse_result) return message.warning(txt)
-  }
+const handleStore = (row) => {
+  const isRecruitment = Boolean(row.task_type === '招聘')
+  // if (isRecruitment) {
+  //   if (!task_source?.data) return message.warning(txt)
+  // } else {
+  //   if (!parse_result) return message.warning(txt)
+  // }
 
-  const list = isRecruitment ? { results: task_source?.data || [] } : parse_result // 解析的人才列表
-  StorePageRef.value.open(task_type, list, id, isDetail)
+  const list = isRecruitment ? { results: [row] } : [row] // 解析的人才列表
+  StorePageRef.value.open(row.task_type, list, row.task_id)
 }
 
 /** 初始化 **/

+ 332 - 0
src/views/menduner/system/talentMap/maintenance/examine/store.vue

@@ -0,0 +1,332 @@
+<template>
+  <Dialog
+    title="人才入库"
+    v-model="dialogVisible"
+    :modalClose="false"
+    width="90%"
+    @close="dialogVisible = false"
+  >
+    <div class="analysisInfoBox">
+      <div class="analysisFile !w-50%">
+        <div v-if="hasSourceUrl">
+          <!-- 门墩儿人才库 -->
+          <template v-if="type === '招聘'">
+            <el-tabs v-model="activeName" type="border-card">
+              <el-tab-pane label="基本信息" name="info">
+                <Info :id="personId" :user-id="userId" @echo="infoEcho" />
+                <expExtend :user-id="userId" defaultShowAll class="m-t-20px" @echo="expEcho" />
+              </el-tab-pane>
+              <el-tab-pane label="附件简历" name="Attachment">
+                <Attachment showPreview :user-id="userId" />
+              </el-tab-pane>
+            </el-tabs>
+          </template>
+          <!-- 简历解析 -->
+          <template v-if="type === '简历'">
+            <IFrame :src="originUrl" />
+          </template>
+          <!-- 名片解析 -->
+          <template v-if="['名片', '杂项'].includes(type)">
+            <div class="image">
+              <el-image class="!w-100%" :src="originUrl" />
+            </div>
+          </template>
+          <!-- 网页解析 -->
+          <template v-if="type === '新任命'">
+            <iframe
+              id="MyIframe"
+              class="!w-100% !h-[calc(100vh-90px)]"
+              src=""
+              frameborder="0"
+            ></iframe>
+          </template>
+        </div>
+        <el-empty v-else description="暂无原始文件" />
+      </div>
+      <div class="flex-1">
+				<!-- <el-tabs type="border-card">
+					<el-tab-pane label="已解析人才列表"> -->
+						<!-- <div class="tagBox mb-10px" v-if="tagList?.length">
+              <el-tag 
+                :type="index === tagCurrentIndex ? 'primary' : 'info'" 
+                size="large"
+                effect="plain"
+                v-for="(val, index) in tagList"
+                :key="val.name + index"
+                class="mr-10px cursor-pointer mb-10px"
+                @click="handleTagClick(index)"
+              >
+                <div class="flex items-center">
+                  <el-checkbox v-model="val.checked" @click.stop />
+                  <span class="ml-5px">{{ val.name }}</span>
+                  <Icon icon="ep:view" class="ml-5px" /> 
+                </div>
+              </el-tag>
+            </div> -->
+            <FormPage	ref="FormPageRef"	formType="create"	:itemData="itemData" />
+					<!-- </el-tab-pane>
+				</el-tabs> -->
+      </div>
+    </div>
+    <template #footer>
+      <el-button @click="handleStore" type="success">入 库</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup>
+/** 人才采集 入库 */
+defineOptions({ name: 'Store' })
+import { ElLoading } from 'element-plus'
+import FormPage from '@/views/menduner/system/talentMap/components/FormPage.vue'
+import Info from '@/views/menduner/system/person/details/components/info.vue'
+import expExtend from '@/views/menduner/system/person/details/components/expExtend.vue'
+import Attachment from '@/views/menduner/system/person/details/components/attachment.vue'
+import { cloneDeep } from 'lodash-es'
+import { marked } from 'marked'
+import { timesTampChange, timestampToAge } from '@/utils/transform/date'
+import { talentGatherApi } from '@/api/menduner/system/talentMap/gather'
+
+const message = useMessage() // 消息弹窗
+
+const emit = defineEmits(['refresh'])
+const dialogVisible = ref(false)
+const FormPageRef = ref(null)
+const itemData = ref({})
+// const tagList = ref([])
+// const tagCurrentIndex = ref(0)
+const type = ref('')
+const originData = ref([])
+
+const originUrl = ref(null)
+const personId = ref('')
+const userId = ref('')
+const activeName = ref('info')
+const hasSourceUrl = ref(false)
+
+// markdown回显
+const showPage = (html) => {
+   // 将 data-src 转化为 src
+  html = html.replace(/data-src/g, 'src')
+    .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/g, '')
+    .replace(/https/g, 'http')
+
+  nextTick(() => {
+    const iframe = document.getElementById('MyIframe')
+    if (!iframe) return
+    const doc = iframe.contentDocument || iframe.document
+    // 设置 iframe 中请求不发送 referrer,以绕过图片防盗链
+    const htmlArr = html.split('</head>')
+    const html_src_add = htmlArr[0] + '<meta name="referrer" content="never"></head>' + htmlArr[1]
+    doc.open()
+    doc.write(html_src_add)
+    doc.close()
+
+    // 设置图片宽高
+    let iwindow = iframe.contentWindow;
+    iframe.addEventListener('load',function () {
+      let idoc = iwindow.document;
+      let imgs = idoc.getElementsByTagName('img')
+      for (let i = 0; i < imgs.length; i++) {
+        const img = imgs[i]
+        if (img) {
+          if (img.width >= img.height) {
+            img.width = iframe.clientWidth / 2
+          } else {
+            img.height = iframe.clientHeight / 2
+            let left = (iframe.clientWidth - img.width) / 2
+            img.style.marginLeft = left + "px"
+          }
+        }
+      }
+    })
+  })
+}
+
+const dealData = async (type, data) => {
+  hasSourceUrl.value = data.image_path ? true : false
+  if (!data.image_path) return
+
+  if (['名片', '杂项', '简历'].includes(type)) {
+    originUrl.value = data.image_path
+  }
+  
+  if (type === '招聘') { // 门墩儿招聘-人员信息在组件中通过id和userId获取
+    personId.value = data?.id || ''
+    userId.value = data?.userId || ''
+    activeName.value = 'info'
+    // return
+  }
+  if (type === '新任命') {
+    await nextTick()
+    const response = await fetch(data.image_path)
+    const text = await response.text()
+
+    const doc = `
+      <!DOCTYPE html>
+      <html class="">
+        <head></head>
+        <body>
+          ${marked(text)}
+        </body>
+      </html>
+    `
+    showPage(doc)
+  }
+}
+
+// 门墩儿招聘人才详情回显赋值
+const infoEcho = (data) => {
+  data = data ? JSON.parse(data) : null
+  itemData.value = {
+    ...itemData.value,
+    name_zh: data?.name || '',
+    email: data?.email || '',
+    mobile: data?.phone || '',
+    birthday: data?.birthday ? timesTampChange(data.birthday, 'Y-M-D') : '',
+    age: data?.birthday ? timestampToAge(data.birthday) : null,
+    created_at: data?.createTime ? timesTampChange(data.createTime, 'Y-M-D') : null,
+    updated_at: data?.updateTime ? timesTampChange(data.updateTime, 'Y-M-D') : null,
+  }
+}
+
+// 门墩儿招聘人才工作经历回显赋值
+const expEcho = (workList) => {
+  itemData.value = {
+    ...itemData.value,
+    career_path: workList ? 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
+  }
+}
+
+// 重置
+const resetData = () => {
+  // tagCurrentIndex.value = 0
+  id.value = null
+  type.value = null
+  originUrl.value = null
+  itemData.value = {}
+  originData.value = []
+  // tagList.value = []
+}
+
+// 打开弹窗
+const id = ref(null)
+const open = async (task_type, parse_result, task_id) => {
+  resetData()
+  id.value = task_id
+  type.value = task_type
+
+  // const dataList = parse_result.map((e, index) => {
+  //   tagList.value.push({
+  //     name: e.name_zh || `人才${ index + 1 }`,
+  //     checked: true
+  //   })
+  //   return e
+  // })
+
+  originData.value = parse_result
+  itemData.value = originData.value[0]
+  dealData(type.value, originData.value[0]) // 赋值左侧原始文件
+  dialogVisible.value = true
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+// 当前操作数据
+// const handleTagClick = (index) => {
+// 	tagCurrentIndex.value = index
+//   const row = originData.value[tagCurrentIndex.value]
+// 	itemData.value = row
+//   dealData(type.value, row)
+// }
+
+// 监听表单变化,同步更新originData中对应的数据
+// watch(() => FormPageRef.value?.formQuery, (newVal) => {
+//   if (tagCurrentIndex.value !== null && originData.value && originData.value.length > tagCurrentIndex.value) {
+//     originData.value[tagCurrentIndex.value] = { ...newVal }
+//   }
+// }, { deep: true })
+
+// 入库
+const handleStore = async () => {
+  // 验证originData数组中每个人才的name_zh字段,并处理mobile字段
+  // const results = [] // 只提交勾选的数据
+  // if (list && Array.isArray(list) && list.length > 0) {
+  //   for (let i = 0; i < list.length; i++) {
+  //     // 只校验勾选的数据
+  //     if (tagList.value[i].checked) {
+  //       const talent = list[i]
+  //       if (!talent.name_zh || talent.name_zh.trim() === '') {
+  //         return message.warning(`第${i + 1}个人才中的中文姓名未填写,请完善后再进行保存`)
+  //         // return message.warning(`中文姓名未填写,请完善后再进行保存`)
+  //       }
+        
+  //       // 处理mobile字段,如果是数组则转换为字符串
+  //       if (Array.isArray(talent.mobile)) {
+  //         talent.mobile = talent.mobile.filter(i => Boolean(i)).map(j => String(j).replace(/,|,/g, '')).join(',');
+  //       }
+  //       results.push(talent)
+  //     }
+  //   }
+  // }
+  
+  // if (!results?.length) return message.warning(`未选择人才,请选择要入库的人才!`)
+  // await message.confirm('是否对当前选中的人才进行入库?')
+
+  const formQuery = FormPageRef.value?.formQuery || {}
+  if (!formQuery?.name_zh) return message.warning(`中文姓名未填写,请完善后再进行保存`)
+  if (Array.isArray(formQuery.mobile)) {
+    formQuery.mobile = formQuery.mobile.filter(i => Boolean(i)).map(j => String(j).replace(/,|,/g, '')).join(',')
+  }
+
+  const loading = ElLoading.service({
+    lock: true,
+    text: '正在保存中...',
+    background: 'rgba(0, 0, 0, 0.7)'
+  })
+  const params = {
+    task_id: id.value,
+    task_type: type.value,
+    data: {
+      results: [formQuery]
+    }
+  }
+  try {
+    await talentGatherApi.talentsAdd(JSON.stringify(params))
+    message.success('入库成功!')
+    dialogVisible.value = false
+    // 刷新列表
+    emit('refresh')
+  } finally {
+    loading.close()
+  }
+}
+</script>
+
+<style lang="scss" scpoed>
+.analysisInfoBox {
+  display: flex;
+	min-height: 50vh;
+  .analysisFile {
+    padding-right: 12px;
+    overflow: auto;
+  }
+}
+// .tagBox {
+//   padding: 12px;
+//   border: 1px dashed #409EFF;
+//   border-radius: 4px;
+// }
+// :deep {
+//   .el-tag__content {
+//     display: flex;
+//     align-items: center;
+//   }
+// }
+</style>

+ 0 - 0
src/views/menduner/system/talentMap/maintenance/gather/components/store.vue → src/views/menduner/system/talentMap/maintenance/examine/storeCopy.vue


+ 210 - 0
src/views/menduner/system/talentMap/maintenance/gather/components/detail.vue

@@ -0,0 +1,210 @@
+<template>
+  <Dialog
+    :title="dialogTitle"
+    v-model="dialogVisible"
+    :modalClose="false"
+    width="90%"
+    @close="dialogVisible = false"
+  >
+    <div class="analysisInfoBox">
+      <div class="analysisFile !w-50%">
+        <div v-if="isDrilling">
+					<!-- 门墩儿招聘 -->
+					<template v-if="source === '招聘'">
+						<el-tabs v-model="activeName" type="border-card">
+							<el-tab-pane label="基本信息" name="info">
+								<Info :id="personId" :user-id="userId" />
+								<expExtend :user-id="userId" defaultShowAll class="m-t-20px" />
+							</el-tab-pane>
+							<el-tab-pane label="附件简历" name="Attachment">
+								<Attachment showPreview :user-id="userId" />
+							</el-tab-pane>
+						</el-tabs>
+					</template>
+					
+					<!-- 简历 -->
+					<template v-if="source === '简历'">
+						<IFrame :src="minio_path" />
+					</template>
+
+					<!-- 名片、杂项 -->
+					<template v-if="['名片', '杂项'].includes(source)">
+						<div class="image">
+							<el-image class="!w-100%" :src="minio_path" :initial-index="0" :preview-src-list="[minio_path]" />
+						</div>
+					</template>
+
+					<!-- 门墩儿新任命 -->
+					<template v-if="source === '新任命'">
+						<iframe
+							id="MyIframe"
+							class="!w-100% !h-[calc(100vh-90px)]"
+							src=""
+							frameborder="0"
+						></iframe>
+					</template>
+				</div>
+				<div v-else class="flex items-center justify-center !h-100%">请在右侧的原始数据选择要查看的项点击详情</div>
+      </div>
+
+      <div class="flex-1">
+				<el-tabs type="border-card">
+          <el-tab-pane label="原始数据">
+						<el-table v-loading="false" :data="list" :stripe="true" border>
+							<el-table-column v-if="source !== '招聘'" label="文件名" align="center" prop="original_filename" />
+							<el-table-column v-if="source === '新任命'" label="发布时间" align="center" prop="publish_time" />
+							<template v-if="source === '招聘'">
+								<el-table-column label="姓名" align="center" prop="data.name_zh" />
+								<el-table-column label="联系电话" align="center" prop="data.mobile" />
+							</template>
+							<el-table-column label="状态" align="center" prop="status">
+								<template #default="scope">{{ source === '招聘' ? '成功' : scope.row.status }}</template>
+							</el-table-column>
+							<el-table-column v-if="list.length > 1" label="操作" align="center" fixed="right" min-width="110">
+								<template #default="scope">
+									<el-button link type="primary" @click="handleDetail(scope.row)">详情</el-button>
+								</template>
+							</el-table-column>
+						</el-table>
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+    </div>
+    <template #footer>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup>
+/** 人才采集 详情 */
+defineOptions({ name: 'GatherDetail' })
+import { marked } from 'marked'
+import Info from '@/views/menduner/system/person/details/components/info.vue'
+import expExtend from '@/views/menduner/system/person/details/components/expExtend.vue'
+import Attachment from '@/views/menduner/system/person/details/components/attachment.vue'
+
+const message = useMessage() // 消息弹窗
+
+const dialogTitle = ref(null)
+const dialogVisible = ref(false)
+const list = ref([])
+const originUrl = ref(null)
+const source = ref(null)
+const typeObj = {
+	'简历': '简历',
+	'名片': '名片',
+	'杂项': '杂项',
+	'招聘': '门墩儿招聘',
+	'新任命': '门墩儿新任命'
+}
+
+const resetData = () => {
+	list.value = []
+	originUrl.value = null
+	source.value = null
+	isDrilling.value = false
+	dialogTitle.value = null
+}
+
+// markdown回显
+const showPage = (html) => {
+   // 将 data-src 转化为 src
+  html = html.replace(/data-src/g, 'src')
+    .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/g, '')
+    .replace(/https/g, 'http')
+
+  nextTick(() => {
+    const iframe = document.getElementById('MyIframe')
+    if (!iframe) return
+    const doc = iframe.contentDocument || iframe.document
+    // 设置 iframe 中请求不发送 referrer,以绕过图片防盗链
+    const htmlArr = html.split('</head>')
+    const html_src_add = htmlArr[0] + '<meta name="referrer" content="never"></head>' + htmlArr[1]
+    doc.open()
+    doc.write(html_src_add)
+    doc.close()
+
+    // 设置图片宽高
+    let iwindow = iframe.contentWindow;
+    iframe.addEventListener('load',function () {
+      let idoc = iwindow.document;
+      let imgs = idoc.getElementsByTagName('img')
+      for (let i = 0; i < imgs.length; i++) {
+        const img = imgs[i]
+        if (img) {
+          if (img.width >= img.height) {
+            img.width = iframe.clientWidth / 2
+          } else {
+            img.height = iframe.clientHeight / 2
+            let left = (iframe.clientWidth - img.width) / 2
+            img.style.marginLeft = left + "px"
+          }
+        }
+      }
+    })
+  })
+}
+
+const minio_path = ref(null)
+const personId = ref(null)
+const userId = ref(null)
+const activeName = ref('info')
+const dealData = async (data) => {
+	const type = source.value
+  if (['名片', '杂项', '简历'].includes(type)) {
+    minio_path.value = data.minio_path
+  }
+  
+	// 门墩儿招聘-人员信息在组件中通过id和userId获取
+  if (type === '招聘') {
+    personId.value = data?.id || ''
+    userId.value = data?.userId || ''
+    activeName.value = 'info'
+  }
+
+  if (type === '新任命') {
+    await nextTick()
+    const response = await fetch(data.minio_path)
+    const text = await response.text()
+
+    const doc = `
+      <!DOCTYPE html>
+      <html class="">
+        <head></head>
+        <body>
+          ${marked(text)}
+        </body>
+      </html>
+    `
+    showPage(doc)
+  }
+}
+
+// 打开弹窗
+const open = async (row) => {
+  resetData()
+	dialogTitle.value = `${typeObj[row.task_type]}【${row.task_name}】详情`
+	source.value = row.task_type
+	list.value = row.task_source || []
+
+  // 只有一条数据时默认展示原始数据
+  if (list.value.length === 1) {
+    isDrilling.value = true
+	  dealData(list.value[0])
+  }
+
+  dialogVisible.value = true
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+// 查看原始文件
+const isDrilling = ref(false)
+const handleDetail = (row) => {
+	isDrilling.value = true
+	dealData(row)
+}
+</script>
+
+<style lang="scss" scpoed>
+</style>

+ 24 - 41
src/views/menduner/system/talentMap/maintenance/gather/components/webAnalysis.vue

@@ -65,7 +65,7 @@ const { t } = useI18n() // 国际化
 
 const queryParams = reactive({
 	// urls: 'https://mp.weixin.qq.com/s/JZ5qxaj9vXsEsswxxD1djA' // https://mp.weixin.qq.com/s/R1aJpn9z-Jf0dk9ttoYYeg
-	// urls: 'https://mp.weixin.qq.com/s/vQLWlSB6DzqSewtBLkk_kQ'
+	// urls: 'https://mp.weixin.qq.com/s/VAANL7NhIp2JogW2ZjSFKg'
   urls: ''
 })
 const queryFormRef = ref()
@@ -90,11 +90,31 @@ turndownService.addRule('wechatImages', {
 })
 
 // 提取主要内容并转换为Markdown文件
-const wechatHtmlToMarkdown = (html, filename = '新任命.md') => {
+const wechatHtmlToMarkdown = (html) => {
   // 创建一个临时DOM解析器
   const parser = new DOMParser()
   const doc = parser.parseFromString(html, 'text/html')
 
+  // 获取文章标题作为文件名
+  let filename = '新任命.md'
+  const title = doc.getElementById('activity-name')?.innerText
+  if (title) {
+    // 检查标题中是否包含"先生"或"女士"
+    const mrIndex = title.indexOf('先生')
+    const msIndex = title.indexOf('女士')
+    
+    if (mrIndex !== -1) {
+      // 如果包含"先生",截取前面的部分
+      filename = title.substring(0, mrIndex).trim() + '.md'
+    } else if (msIndex !== -1) {
+      // 如果包含"女士",截取前面的部分
+      filename = title.substring(0, msIndex).trim() + '.md'
+    } else {
+      // 如果不包含,使用原标题
+      filename = title.trim() + '.md'
+    }
+  }
+
   // 提取正文内容 - 微信公众号文章通常在id="js_content"的div中
   const content = doc.querySelector('#js_content') || doc.body
 
@@ -108,8 +128,6 @@ const wechatHtmlToMarkdown = (html, filename = '新任命.md') => {
     content.querySelectorAll(selector).forEach(el => el.remove())
   })
 
-  // return turndownService.turndown(content.innerHTML)
-
   // 转换为Markdown
   const result = turndownService.turndown(content.innerHTML)
 
@@ -117,14 +135,6 @@ const wechatHtmlToMarkdown = (html, filename = '新任命.md') => {
   return new File([blob], filename, { type: 'text/markdown' })
 }
 
-// 转换为markdown格式
-// const handleConvert = (res) => {
-// 	if (!res.data) return
-// 	const result = wechatHtmlToMarkdown(res.data)
-// 	if (!result) return message.warning('转换失败')
-// 	return result
-// }
-
 function extractPublishTime(doc, html) {
   // 1. 通过 id
   let timeEl = doc.getElementById('publish_time')
@@ -165,7 +175,7 @@ function tryExtractPublishTime(doc, html, cb, maxTry = 10, interval = 200) {
 const showPage = (res) => {
   let html = res.data
   if (!html) return
-  
+
    // 将 data-src 转化为 src
   html = html.replace(/data-src/g, 'src')
     // 需要获取文章发布时间的话需注释下一行代码
@@ -202,8 +212,7 @@ const showPage = (res) => {
             .replace("年", "-")
             .replace("月", "-")
             .replace("日", "")
-            .split(" ")[0];
-          console.log(publishTime, '发布时间', res.publish_time)
+            .split(" ")[0]
         }
       });
     }, 100); // 先等100ms让iframe初步渲染,再开始轮询
@@ -248,7 +257,6 @@ const handleAnalysis = async () => {
 				publish_time: null,
 				id: generateUUID(),
         file: wechatHtmlToMarkdown(e.data)
-				// markdown_text: handleConvert(e)
 			})
 		})
 		contents.value.forEach(e => {
@@ -263,31 +271,6 @@ const handleAnalysis = async () => {
     loading.close()
 	})
 }
-
-// 信息提取
-// const handleSubmit = async (content, index) => {
-// 	if (!content.markdown_text) return
-
-//   const loading = ElLoading.service({
-//     lock: true,
-//     text: '信息正在提取中...',
-//     background: 'rgba(0, 0, 0, 0.7)',
-//   })
-
-//   const { markdown_text, publish_time } = content
-// 	if (!publish_time) {
-// 		message.warning('发布时间不能为空')
-//     loading.close()
-// 		return
-// 	}
-// 	try {
-// 		const data = await talentWebParsingApi.saveMarkdownContent({ markdown_text, publish_time })
-// 		emit('analysis', data ?? [], markdown_text)
-// 		message.success('信息提取成功')
-// 	} finally {
-//     loading.close()
-// 	}
-// }
 </script>
 
 <style scoped>

+ 23 - 43
src/views/menduner/system/talentMap/maintenance/gather/index.vue

@@ -27,21 +27,23 @@
           class="!w-200px"
           @change="handleQuery"
         >
-          <el-option v-for="(val, index) in taskStatus" :label="val" :value="val" :key="index" />
+          <el-option v-for="(val, index) in ['待解析', '解析成功']" :label="val" :value="val" :key="index" />
         </el-select>
       </el-form-item>
-      <el-form-item>
+      <div class="text-center mb-12px">
         <el-button @click="handleQuery"><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>
+      </div>
     </el-form>
   </ContentWrap>
 
   <!-- 列表 -->
   <ContentWrap>
+    <div class="text-right mb-10px">
+      <el-button type="primary" plain @click="handleAdd">
+        <Icon icon="ep:plus" class="mr-5px" /> 新增人才
+      </el-button>
+    </div>
     <el-table v-loading="loading" :data="list" :stripe="true">
       <el-table-column label="任务ID" align="center" prop="id" />
       <el-table-column label="任务名称" align="center" prop="task_name" />
@@ -52,18 +54,14 @@
       <el-table-column label="操作" align="center" fixed="right" min-width="110">
         <template #default="scope">
           <el-button
-            v-if="scope.row.task_status === '待解析'"
+            v-if="['待解析', '不成功'].includes(scope.row.task_status)"
             link
-            type="primary"
+            type="success"
             @click="handleAnalysis(scope.row)"
-          >解析</el-button>
-          <el-button v-else type="success" link @click="handleStore(scope.row, true)">解析结果</el-button>
-          <el-button
-            v-if="scope.row.task_status === '成功'"
-            link
-            type="warning"
-            @click="handleStore(scope.row)"
-          >入库</el-button>
+          >
+            解析
+          </el-button>
+          <el-button link type="primary" @click="handleDetail(scope.row)">详情</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -167,7 +165,7 @@
     </template>
   </Dialog>
 
-  <StorePage ref="StorePageRef" @refresh="handleQuery" />
+  <TaskDetail ref="TaskDetailRef" />
 </template>
 
 <script setup>
@@ -185,8 +183,8 @@ import expExtend from '@/views/menduner/system/person/details/components/expExte
 import Attachment from '@/views/menduner/system/person/details/components/attachment.vue'
 import { talentWebParsingApi } from '@/api/menduner/system/talentMap/webParsing'
 import { ElLoading } from 'element-plus'
-import StorePage from '@/views/menduner/system/talentMap/maintenance/gather/components/store.vue'
 import { talentGatherApi } from '@/api/menduner/system/talentMap/gather'
+import TaskDetail from './components/detail.vue'
 
 const { uploadUrl, httpRequest } = useUpload()
 const message = useMessage() // 消息弹窗
@@ -213,7 +211,6 @@ const taskType = [
   { label: '门墩儿招聘', value: '招聘' },
   { label: '杂项', value: '杂项' }
 ]
-const taskStatus = ['待解析', '成功', '已入库']
 const itemData = ref({})
 
 const SearchRef = ref(null)
@@ -249,25 +246,15 @@ const handleWebAnalysis = (list) => {
   webOriginList.value = list ?? []
 }
 
-// 入库
-const StorePageRef = ref(null)
-const handleStore = ({ task_type, parse_result, id, task_source }, isDetail = false) => {
-  const txt = isDetail ? '暂无详情查看' : '暂无解析数据可入库'
-  const isRecruitment = Boolean(task_type === '招聘')
-  if (isRecruitment) {
-    if (!task_source?.data) return message.warning(txt)
-  } else {
-    if (!parse_result) return message.warning(txt)
-  }
-
-  const list = isRecruitment ? { results: task_source?.data || [] } : parse_result // 解析的人才列表
-  StorePageRef.value.open(task_type, list, id, isDetail)
+// 任务详情
+const TaskDetailRef = ref(null)
+const handleDetail = (row) => {
+  TaskDetailRef.value.open(row)
 }
 
 // 任务解析
-const handleAnalysis = async ({ task_type, id, task_source, task_name }) => {
-  if (!id) return
-  await message.confirm('是否解析当前任务?')
+const handleAnalysis = async (row) => {
+  await message.confirm('是否确认解析当前任务?')
 
   const loading = ElLoading.service({
     lock: true,
@@ -275,15 +262,8 @@ const handleAnalysis = async ({ task_type, id, task_source, task_name }) => {
     background: 'rgba(0, 0, 0, 0.7)',
   })
 
-  const params = {
-    task_type,
-    id,
-    data: task_source.minio_paths_json
-  }
-  if (task_type === '杂项') params.process_type = 'table'
-  if (task_type === '新任命') params.publish_time = task_source.publish_time
   try {
-    await talentGatherApi.taskAnalysis(params)
+    await talentGatherApi.taskAnalysis({ data: row })
     message.success('解析成功')
     handleQuery()
   } finally {

+ 384 - 0
src/views/menduner/system/talentMap/maintenance/labeling/LabelingForm copy.vue

@@ -0,0 +1,384 @@
+<template>
+  <Dialog title="人才标注" v-model="dialogVisible" class="!w-90%">
+		<el-row :gutter="20">
+			<el-col :span="9">
+				<el-card>
+					<div>
+						<p :style="{'color': previewUrl ? '#2d8cf0' : ''}">名片</p>
+						<el-image v-if="previewUrl" width="100%" :preview-src-list="[previewUrl]" class="cursor-pointer" :src="previewUrl" />
+					</div>
+					<p>门墩儿用户简历</p>
+					<div>
+						<p :style="{'color': markdown_text ? '#2d8cf0' : ''}">门墩儿新任命</p>
+						<iframe
+							v-if="markdown_text"
+							id="Iframe"
+							class="markdownContent"
+							src=""
+							frameborder="0"
+						></iframe>
+					</div>
+				</el-card>
+			</el-col>
+			<el-col :span="15">
+				<div class="!h-100% overflow-y-auto">
+					<el-tabs type="border-card">
+						<el-tab-pane label="人才信息">
+							<FormPage ref="baseInfoRef" formType="edit" :itemData="talentItem" />
+							<div class="text-right mt-12px">
+								<el-button @click="handleSave" type="primary" :disabled="saveLoading">保 存</el-button>
+								<el-button @click="dialogVisible = false">取消</el-button>
+							</div>
+						</el-tab-pane>
+						<el-tab-pane label="人才标签">
+							<el-card shadow="never">
+								<div class="my-5">
+									<div class="mt-4 px-3 pb-3" style="border: 1px dashed #67c23a; border-radius: 4px;">
+										<div v-if="talentSelectedTags?.length">
+											<el-tag 
+												v-for="(item, index) in talentSelectedTags" 
+												:key="index"
+												closable
+												size="large"
+												type="success"
+												class="mr-14px mt-14px"
+												@close="closeClick(item)"
+											>
+												{{ item.name }}
+											</el-tag>
+										</div>
+										<div v-else style="color: #777; text-align: center; line-height: 50px; margin-top: 12px;">请添加标签</div>
+									</div>
+									
+									<div :class="{'mt-5': talentSelectedTags?.length > 0}">
+										<el-tag 
+											v-for="(item, index) in tagList" :key="index"
+											size="large"
+											type="primary"
+											class="mr-14px mt-14px"
+											:class="{'cursor-pointer': !talentSelectedTags.find(k => k.name === item.name)}"
+											:effect="talentSelectedTags.find(k => k.name === item.name) ? 'info' : 'default'"
+											@click="handleAdd(item)"
+										>
+											+ {{ item.name }}
+										</el-tag>
+									</div>
+								</div>
+							</el-card>
+							<div class="text-right mt-12px">
+								<el-button @click="submitForm" type="primary" :disabled="loading">保 存</el-button>
+								<el-button @click="dialogVisible = false">取消</el-button>
+							</div>
+						</el-tab-pane>
+					</el-tabs>
+				</div>
+			</el-col>
+		</el-row>
+  </Dialog>
+</template>
+
+<script setup>
+import { talentLabelingApi } from '@/api/menduner/system/talentMap/labeling'
+import { talentTagApi } from '@/api/menduner/system/talentMap/tag'
+import DefaultData from '@/views/menduner/system/talentMap/details/defaultData'
+import { getDict } from '@/hooks/web/useDictionaries'
+import { cloneDeep } from 'lodash-es'
+import { TalentMap } from '@/api/menduner/system/talentMap'
+import FormPage from '@/views/menduner/system/talentMap/components/FormPage.vue'
+import { marked } from 'marked'
+
+const message = useMessage() // 消息弹窗
+const loading = ref(false)
+const dialogVisible = ref(false) // 弹窗的是否展示
+const talentItem = ref({})
+const previewUrl = ref()
+const talentSelectedTags = ref([])
+const tagList = ref([])
+
+const result = ref(cloneDeep(DefaultData))
+
+// 获取人才标签
+const getTagList = async () => {
+	loading.value = true
+	try {
+		const data = await talentTagApi.getTalentTagList()
+		tagList.value = data || []
+	} finally {
+		loading.value = false
+	}
+}
+
+// 地区树状列表
+const areaTreeData = ref([])
+const getDictData = async () => {
+  const { data } = await getDict('areaTreeData', {}, 'areaTreeData')
+  const obj = data.find(e => e.name === '中国')
+  const list = obj?.children ? obj.children.map(e =>{
+    // 市辖区直接显示区
+    const municipality = e.children && e.children.length && e.children[0].name === '市辖区'
+    if (municipality && e.children[0].children?.length) e.children = e.children[0].children
+    return e
+  }) : []
+  areaTreeData.value = list.length ? list : []
+}
+getDictData()
+
+// 获取人才标签
+const getTalentTagById = async() => {
+	const id = talentItem.value?.id
+	if (!id) {
+		talentSelectedTags.value = []
+		return
+	}
+	const tagData = await talentLabelingApi.getTalentTagById(id)
+	talentSelectedTags.value = tagData ? tagData.map((i) => {
+		return { id: i.talent, name: i.tag }
+	}) : []
+}
+
+// 存储observer引用,用于销毁
+let currentObserver = null
+
+// 查看原网页
+const showPage = (html) => {
+  // 预处理HTML内容
+  html = html.replace(/data-src/g, 'src') // 将 data-src 转化为 src
+    .replace(/https/g, 'http') // 将HTML内容中所有的https替换为http
+    
+  // 创建完整的HTML文档,包含强制图片样式的CSS
+  const fullHtml = `
+    <!DOCTYPE html>
+    <html>
+    <head>
+      <meta charset="utf-8">
+      <meta name="referrer" content="never">
+      <style>
+        * {
+          box-sizing: border-box;
+        }
+        body {
+          margin: 0;
+          padding: 0;
+          width: 100%;
+          max-width: 100%;
+          overflow-x: hidden;
+        }
+        img {
+          width: 100% !important;
+          max-width: 100% !important;
+          height: auto !important;
+          object-fit: contain !important;
+          display: block !important;
+        }
+      </style>
+    </head>
+    <body>
+      ${html}
+    </body>
+    </html>
+  `
+  
+  nextTick(() => {
+    const iframe = document.getElementById('Iframe')
+    if (!iframe) return
+    
+    // 销毁之前的observer
+    if (currentObserver) {
+      currentObserver.disconnect()
+      currentObserver = null
+    }
+    
+    const doc = iframe.contentDocument || iframe.document
+    doc.open()
+    doc.write(fullHtml)
+    doc.close()
+    
+    // 等待内容加载完成后进一步处理
+    setTimeout(() => {
+      // 确保js_content可见
+      const jsContent = doc.getElementById('js_content')
+      if (jsContent) {
+        jsContent.style.visibility = 'visible'
+        jsContent.style.opacity = 1
+      }
+      
+      // 强制重新设置所有图片样式
+      const images = doc.querySelectorAll('img')
+      images.forEach(img => {
+        img.style.setProperty('width', '100%', 'important')
+        img.style.setProperty('max-width', '100%', 'important')
+        img.style.setProperty('height', 'auto', 'important')
+        img.style.setProperty('object-fit', 'contain', 'important')
+        img.style.setProperty('display', 'block', 'important')
+        img.removeAttribute('width')
+        img.removeAttribute('height')
+      })
+      
+      // 设置所有容器
+      const containers = doc.querySelectorAll('div, section, article, p')
+      containers.forEach(container => {
+        container.style.setProperty('width', '100%', 'important')
+        container.style.setProperty('max-width', '100%', 'important')
+        container.style.setProperty('overflow', 'hidden', 'important')
+      })
+      
+      // 创建新的observer并保存引用
+      currentObserver = new MutationObserver((mutations) => {
+        mutations.forEach((mutation) => {
+          mutation.addedNodes.forEach((node) => {
+            if (node.nodeType === 1) {
+              const newImages = node.querySelectorAll ? node.querySelectorAll('img') : []
+              if (node.tagName === 'IMG') newImages.push(node)
+              
+              newImages.forEach(img => {
+                img.style.setProperty('width', '100%', 'important')
+                img.style.setProperty('max-width', '100%', 'important')
+                img.style.setProperty('height', 'auto', 'important')
+                img.style.setProperty('object-fit', 'contain', 'important')
+                img.style.setProperty('display', 'block', 'important')
+                img.removeAttribute('width')
+                img.removeAttribute('height')
+              })
+            }
+          })
+        })
+      })
+      
+      currentObserver.observe(doc.body, {
+        childList: true,
+        subtree: true
+      })
+    }, 200) // 增加延时确保内容完全加载
+  })
+}
+
+/** 打开弹窗 */
+const markdown_text = ref(null)
+const open = async (data) => {
+	console.log(data, 'open')
+	previewUrl.value = null
+  dialogVisible.value = true
+	talentItem.value = data
+
+	// 获取所有人才标签
+	await getTagList()
+
+	// 获取名片预览
+	// if (data.image_path) {
+	// 	const res = await talentLabelingApi.getTalentCardByImagePath(data.image_path)
+	// 	previewUrl.value = URL.createObjectURL(res)
+	// }
+
+	// 网页解析原始markdown内容
+	// if (data.origin_source) {
+	// 	if (!data.origin_source?.minio_path) return
+	// 	const result = await talentLabelingApi.getTalentMarkdown(data.origin_source.minio_path)
+	// 	if (result) {
+	// 		markdown_text.value = marked(result)
+	// 		if (markdown_text.value) showPage(markdown_text.value)
+	// 	}
+	// }
+
+	// 获取人才标签
+	await getTalentTagById()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+// 标签删除
+const closeClick = (item) => {
+	const index = talentSelectedTags.value.findIndex((i) => i === item)
+	if (index !== -1) talentSelectedTags.value.splice(index, 1)
+}
+
+// 标签添加
+const handleAdd = (item) => {
+	talentSelectedTags.value.push(item)
+}
+
+/** 人才标签保存 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+	if (!talentSelectedTags.value || !talentSelectedTags.value.length) return message.warning('请选择要更新的人才标签')
+
+	loading.value = true
+	const tags = talentSelectedTags.value.map(e => {
+		return { talent: talentItem.value.id, tag: e.name }
+	})
+
+  // 提交请求
+  try {
+    await talentLabelingApi.updateTalentTags(tags)
+		message.success('人才标签更新成功')
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 保存人才信息
+const baseInfoRef = ref(null)
+const saveLoading = ref(false)
+const handleSave = async () => {
+	saveLoading.value = true
+	const params = baseInfoRef.value.formQuery
+	if (!params || !Object.keys(params).length) return saveLoading.value = true
+	// 数组转为字符串保存
+  if (Array.isArray(params?.mobile)) {
+    params.mobile = params.mobile.filter(i => Boolean(i)).map(j => String(j).replace(/,|,/g, '')).join(',');
+  }
+	try {
+		await talentLabelingApi.updateBusinessCard({...params, origin_source: talentItem.value?.origin_source}, talentItem.value.id)
+		dialogVisible.value = false
+		message.success('保存成功')
+		emit('success')
+	} finally {
+	  saveLoading.value = false
+	}
+}
+
+// 清理函数,用于销毁observer
+const cleanup = () => {
+  if (currentObserver) {
+    currentObserver.disconnect()
+    currentObserver = null
+  }
+}
+
+// 监听弹窗关闭,清理observer
+watch(dialogVisible, (newVal) => {
+  if (!newVal) {
+    cleanup()
+  }
+})
+
+// 组件卸载时清理
+onUnmounted(() => {
+  cleanup()
+})
+
+</script>
+
+<style scoped lang="scss">
+.base-info {
+	background-color: #f7f8fa;
+	border-radius: 6px;
+	padding: 15px;
+}
+:deep {
+	.el-descriptions__content {
+		color: #303133;
+	}
+	.el-descriptions__body {
+		background-color: #f7f8fa;
+	}
+	.markdownContent {
+		width: 100%;
+		max-width: 100%;
+		height: 600px;
+		border: none;
+		overflow: hidden;
+	}
+}
+</style>

+ 101 - 174
src/views/menduner/system/talentMap/maintenance/labeling/LabelingForm.vue

@@ -2,22 +2,37 @@
   <Dialog title="人才标注" v-model="dialogVisible" class="!w-90%">
 		<el-row :gutter="20">
 			<el-col :span="9">
-				<el-card>
-					<div>
-						<p :style="{'color': previewUrl ? '#2d8cf0' : ''}">名片</p>
-						<el-image v-if="previewUrl" width="100%" :preview-src-list="[previewUrl]" class="cursor-pointer" :src="previewUrl" />
-					</div>
-					<p>门墩儿用户简历</p>
-					<div>
-						<p :style="{'color': markdown_text ? '#2d8cf0' : ''}">门墩儿新任命</p>
-						<iframe
-							v-if="markdown_text"
-							id="Iframe"
-							class="markdownContent"
-							src=""
-							frameborder="0"
-						></iframe>
-					</div>
+				<div v-if="sourceList && sourceList.length > 0" class="!h-80vh overflow-y-auto">
+					<el-card v-for="val in sourceList" :key="val.id" class="mb-10px">
+						<template #header>
+							<CardTitle :title="typeObj[val.task_type] + val.source_date" />
+						</template>
+						<el-image 
+							v-if="['名片', '杂项'].includes(val.task_type)"
+							width="100%"
+							:preview-src-list="[val.minio_path]"
+							class="cursor-pointer"
+							:src="val.minio_path"
+						/>
+
+						<p v-if="val.task_type === '招聘'">人才ID:{{ val.minio_path }}</p>
+
+						<template v-if="val.task_type === '简历'">
+							<IFrame :src="val.minio_path" />
+						</template>
+
+						<template v-if="val.task_type === '新任命'">
+							<iframe
+								:id="val.id"
+								class="markdownContent"
+								src=""
+								frameborder="0"
+							></iframe>
+						</template>
+					</el-card>
+				</div>
+				<el-card v-else>
+					<el-empty description="暂无来源" />
 				</el-card>
 			</el-col>
 			<el-col :span="15">
@@ -86,6 +101,7 @@ import { cloneDeep } from 'lodash-es'
 import { TalentMap } from '@/api/menduner/system/talentMap'
 import FormPage from '@/views/menduner/system/talentMap/components/FormPage.vue'
 import { marked } from 'marked'
+import { generateUUID } from '@/utils'
 
 const message = useMessage() // 消息弹窗
 const loading = ref(false)
@@ -97,6 +113,14 @@ const tagList = ref([])
 
 const result = ref(cloneDeep(DefaultData))
 
+const typeObj = {
+	'招聘': '门墩儿招聘',
+	'新任命': '门墩儿新任命',
+	'名片': '名片',
+	'简历': '简历',
+	'杂项': '杂项'
+}
+
 // 获取人才标签
 const getTagList = async () => {
 	loading.value = true
@@ -108,21 +132,6 @@ const getTagList = async () => {
 	}
 }
 
-// 地区树状列表
-const areaTreeData = ref([])
-const getDictData = async () => {
-  const { data } = await getDict('areaTreeData', {}, 'areaTreeData')
-  const obj = data.find(e => e.name === '中国')
-  const list = obj?.children ? obj.children.map(e =>{
-    // 市辖区直接显示区
-    const municipality = e.children && e.children.length && e.children[0].name === '市辖区'
-    if (municipality && e.children[0].children?.length) e.children = e.children[0].children
-    return e
-  }) : []
-  areaTreeData.value = list.length ? list : []
-}
-getDictData()
-
 // 获取人才标签
 const getTalentTagById = async() => {
 	const id = talentItem.value?.id
@@ -136,147 +145,86 @@ const getTalentTagById = async() => {
 	}) : []
 }
 
-// 存储observer引用,用于销毁
-let currentObserver = null
+// markdown回显
+const showPage = (id, html) => {
+   // 将 data-src 转化为 src
+  html = html.replace(/data-src/g, 'src')
+    .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/g, '')
+    .replace(/https/g, 'http')
 
-// 查看原网页
-const showPage = (html) => {
-  // 预处理HTML内容
-  html = html.replace(/data-src/g, 'src') // 将 data-src 转化为 src
-    .replace(/https/g, 'http') // 将HTML内容中所有的https替换为http
-    
-  // 创建完整的HTML文档,包含强制图片样式的CSS
-  const fullHtml = `
-    <!DOCTYPE html>
-    <html>
-    <head>
-      <meta charset="utf-8">
-      <meta name="referrer" content="never">
-      <style>
-        * {
-          box-sizing: border-box;
-        }
-        body {
-          margin: 0;
-          padding: 0;
-          width: 100%;
-          max-width: 100%;
-          overflow-x: hidden;
-        }
-        img {
-          width: 100% !important;
-          max-width: 100% !important;
-          height: auto !important;
-          object-fit: contain !important;
-          display: block !important;
-        }
-      </style>
-    </head>
-    <body>
-      ${html}
-    </body>
-    </html>
-  `
-  
   nextTick(() => {
-    const iframe = document.getElementById('Iframe')
+    const iframe = document.getElementById(id)
     if (!iframe) return
-    
-    // 销毁之前的observer
-    if (currentObserver) {
-      currentObserver.disconnect()
-      currentObserver = null
-    }
-    
     const doc = iframe.contentDocument || iframe.document
+    // 设置 iframe 中请求不发送 referrer,以绕过图片防盗链
+    const htmlArr = html.split('</head>')
+    const html_src_add = htmlArr[0] + '<meta name="referrer" content="never"></head>' + htmlArr[1]
     doc.open()
-    doc.write(fullHtml)
+    doc.write(html_src_add)
     doc.close()
-    
-    // 等待内容加载完成后进一步处理
-    setTimeout(() => {
-      // 确保js_content可见
-      const jsContent = doc.getElementById('js_content')
-      if (jsContent) {
-        jsContent.style.visibility = 'visible'
-        jsContent.style.opacity = 1
+
+    // 设置图片宽高
+    let iwindow = iframe.contentWindow;
+    iframe.addEventListener('load',function () {
+      let idoc = iwindow.document;
+      let imgs = idoc.getElementsByTagName('img')
+      for (let i = 0; i < imgs.length; i++) {
+        const img = imgs[i]
+        if (img) {
+          if (img.width >= img.height) {
+            img.width = iframe.clientWidth / 2
+          } else {
+            img.height = iframe.clientHeight / 2
+            let left = (iframe.clientWidth - img.width) / 2
+            img.style.marginLeft = left + "px"
+          }
+        }
       }
-      
-      // 强制重新设置所有图片样式
-      const images = doc.querySelectorAll('img')
-      images.forEach(img => {
-        img.style.setProperty('width', '100%', 'important')
-        img.style.setProperty('max-width', '100%', 'important')
-        img.style.setProperty('height', 'auto', 'important')
-        img.style.setProperty('object-fit', 'contain', 'important')
-        img.style.setProperty('display', 'block', 'important')
-        img.removeAttribute('width')
-        img.removeAttribute('height')
-      })
-      
-      // 设置所有容器
-      const containers = doc.querySelectorAll('div, section, article, p')
-      containers.forEach(container => {
-        container.style.setProperty('width', '100%', 'important')
-        container.style.setProperty('max-width', '100%', 'important')
-        container.style.setProperty('overflow', 'hidden', 'important')
-      })
-      
-      // 创建新的observer并保存引用
-      currentObserver = new MutationObserver((mutations) => {
-        mutations.forEach((mutation) => {
-          mutation.addedNodes.forEach((node) => {
-            if (node.nodeType === 1) {
-              const newImages = node.querySelectorAll ? node.querySelectorAll('img') : []
-              if (node.tagName === 'IMG') newImages.push(node)
-              
-              newImages.forEach(img => {
-                img.style.setProperty('width', '100%', 'important')
-                img.style.setProperty('max-width', '100%', 'important')
-                img.style.setProperty('height', 'auto', 'important')
-                img.style.setProperty('object-fit', 'contain', 'important')
-                img.style.setProperty('display', 'block', 'important')
-                img.removeAttribute('width')
-                img.removeAttribute('height')
-              })
-            }
-          })
-        })
-      })
-      
-      currentObserver.observe(doc.body, {
-        childList: true,
-        subtree: true
-      })
-    }, 200) // 增加延时确保内容完全加载
+    })
   })
 }
 
+const handleShowPage = async (id, url) => {
+	await nextTick()
+	const response = await fetch(url)
+	const text = await response.text()
+	const doc = `
+		<!DOCTYPE html>
+		<html class="">
+			<head></head>
+			<body>
+				${marked(text)}
+			</body>
+		</html>
+	`
+	showPage(id, doc)
+}
+
 /** 打开弹窗 */
-const markdown_text = ref(null)
+const sourceList = ref([])
 const open = async (data) => {
+	sourceList.value = []
 	previewUrl.value = null
   dialogVisible.value = true
 	talentItem.value = data
 
-	// 获取所有人才标签
-	await getTagList()
+	if (data?.origin_source) {
+		const list = JSON.parse(data.origin_source)
+		// 时间排序
+		const sortedList = list.sort((a, b) => new Date(b.source_date) - new Date(a.source_date))
+		sourceList.value = sortedList.map(e => {
+			const item = { ...e, id: generateUUID() }
 
-	// 获取名片预览
-	if (data.image_path) {
-		const res = await talentLabelingApi.getTalentCardByImagePath(data.image_path)
-		previewUrl.value = URL.createObjectURL(res)
+			if (e.task_type === '新任命') {
+				handleShowPage(item.id, item.minio_path)
+			}
+			
+			return item
+		})
 	}
 
-	// 网页解析原始markdown内容
-	if (data.origin_source) {
-		if (!data.origin_source?.minio_path) return
-		const result = await talentLabelingApi.getTalentMarkdown(data.origin_source.minio_path)
-		if (result) {
-			markdown_text.value = marked(result)
-			if (markdown_text.value) showPage(markdown_text.value)
-		}
-	}
+	// 获取所有人才标签
+	await getTagList()
 
 	// 获取人才标签
 	await getTalentTagById()
@@ -336,27 +284,6 @@ const handleSave = async () => {
 	  saveLoading.value = false
 	}
 }
-
-// 清理函数,用于销毁observer
-const cleanup = () => {
-  if (currentObserver) {
-    currentObserver.disconnect()
-    currentObserver = null
-  }
-}
-
-// 监听弹窗关闭,清理observer
-watch(dialogVisible, (newVal) => {
-  if (!newVal) {
-    cleanup()
-  }
-})
-
-// 组件卸载时清理
-onUnmounted(() => {
-  cleanup()
-})
-
 </script>
 
 <style scoped lang="scss">

+ 2 - 2
src/views/menduner/system/talentMap/maintenance/labeling/index.vue

@@ -35,10 +35,10 @@
           class="!w-240px"
         />
       </el-form-item>
-      <el-form-item>
+      <div class="text-center mb-12px">
         <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-      </el-form-item>
+      </div>
     </el-form>
   </ContentWrap>
 

+ 3 - 3
src/views/menduner/system/web/WebContentForm.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
+  <Dialog :title="dialogTitle" v-model="dialogVisible" class="!w-60%">
     <el-form
       ref="formRef"
       :model="formData"
@@ -63,10 +63,10 @@
 </template>
 
 <script setup>
-import { WebContentApi } from '@/api/menduner/system/web'
-
 /** 页面内容 表单 */
 defineOptions({ name: 'WebContentForm' })
+import { WebContentApi } from '@/api/menduner/system/web'
+
 defineProps({ enterpriseList: Array })
 
 const { t } = useI18n() // 国际化