Xiao_123 hai 2 meses
pai
achega
493de4aecb

+ 13 - 0
api/enterprise.js

@@ -61,4 +61,17 @@ export const updateEnterpriseLogo = (url) => {
       auth: true
     }
   })
+}
+
+// 获取人才的在线简历详情
+export const getPersonCvDetail = (userId) => {
+  return request({
+    url: '/app-api/menduner/system/recruit/person-cv/detail?userId=' + userId,
+    method: 'GET',
+    custom: {
+      openEncryption: true,
+      showLoading: false,
+      auth: false
+    }
+  })
 }

+ 8 - 2
pages.json

@@ -13,9 +13,9 @@
 			}
 		},
 		{
-			"path": "pages/login/index",
+			"path": "pages/register/index",
 			"style": {
-				"navigationBarTitleText": "登录/注册"
+				"navigationBarTitleText": "企业注册"
 			}
 		},
 		{
@@ -82,6 +82,12 @@
 		{
 			"root": "pagesB",
 			"pages": [
+				{
+					"path": "personnelDetails/index",
+					"style": {
+						"navigationBarTitleText": "人才详情"
+					}
+				},
 				{
 					"path": "contactUs/index",
 					"style": {

+ 9 - 1
pages/index/resume.vue

@@ -1,5 +1,7 @@
 <template>
-	<layout-page>企业-简历</layout-page>
+	<layout-page>
+		<view @tap="handleTo">企业-简历</view>
+	</layout-page>
 </template>
 
 <script setup>
@@ -14,6 +16,12 @@
 	    // 设置当前tab页的下标index
 	    currentTabBar?.setData({ selected: 0 });
 	})
+
+	const handleTo = () => {
+		uni.navigateTo({
+			url: '/pagesB/personnelDetails/index?id=1'
+		})
+	}
 </script>
 
 <style scoped lang="scss">

+ 30 - 0
pagesB/personnelDetails/components/advantage.vue

@@ -0,0 +1,30 @@
+<template>
+	<rich-text v-if="data" class="htmlCss" :nodes="cleanedHtml(data)"></rich-text>
+</template>
+
+<script setup>
+defineProps({ data: String })
+
+const cleanedHtml = (text) => {
+  const cleaned = text.replace(/\n/g, '<br>')
+  .replace(/\s+/g, ' ')
+  .replace(/(^|\s+)<\/p>(\s*<p>|$)/g, '</p><p>')
+  .replace(/<p>\s*(<br>)\s*<\/p>/g, '')
+  .replace(/<pre([^>]*)>/g, '<div$1>')
+  .replace(/<\/pre>/g, '</div>')
+  .replace(/<p>\s*(<\/br>)\s*<\/p>/g, '').trim()
+  return cleaned
+}
+</script>
+
+<style scoped lang="scss">
+.htmlCss {
+  white-space: pre-wrap;
+  word-break: break-all;
+  line-height: 28px;
+  color: var(--color-333);
+  font-size: 15px;
+  text-align: justify;
+  letter-spacing: 0;
+}
+</style>

+ 56 - 0
pagesB/personnelDetails/components/baseInfo.vue

@@ -0,0 +1,56 @@
+<template>
+	<view>
+		<view class="d-flex">
+			<view class="user-left">
+				<view class="user-name">{{ data?.name }}</view>
+				<view class="ss-m-t-10">
+					<span 
+						class="color-666"
+					 	v-for="(key, index) in desc" 
+					 	:key="index"
+					>
+						{{ data[key] }}
+						<span v-if="index !== desc.length - 1 && data[key]" class="ss-m-x-10">|</span>
+					</span>
+				</view>
+			</view>
+			<view class="user-avatar">
+				<image :src="data?.avatar" alt="" mode="scaleToFill" />
+			</view>
+		</view>
+		<view class="ss-m-t-10">
+      <uni-tag 
+        v-for="(tag,i) in data?.tagList || []"
+        :key="i"
+        class="tag-gap ss-m-r-10"
+        :text="tag"
+        inverted="false"
+        size="medium"
+        custom-style="background-color: #ececec; color: #666; border-color: #ececec; display: inline-block;"
+      />
+    </view>
+	</view>
+</template>
+
+<script setup>
+const props = defineProps({ data: Object })
+console.log(props.data, 'props.data')
+
+const desc = ['jobStatusName', 'eduName', 'expName']
+</script>
+
+<style scoped lang="scss">
+.user-avatar image {
+	width: 65px;
+	height: 65px;
+	border-radius: 50%;
+}
+.user-left {
+	flex: 1;
+	.user-name {
+		font-size: 25px;
+		font-weight: 600;
+		color: #0E100F;
+	}
+}
+</style>

+ 58 - 0
pagesB/personnelDetails/components/eduExp.vue

@@ -0,0 +1,58 @@
+<template>
+	<view class="content">
+    <view
+      v-for="val in data"
+      :key="val.id"
+      class="content-item"
+    >
+      <view class="content-title">
+        <view class="name">{{ val.schoolName }}</view>
+        <view class="time">
+          {{ timesTampChange(val.startTime ,'Y-M') }} - {{ timesTampChange(val.endTime ,'Y-M') }}
+        </view>
+      </view>
+      <view class="content-subTitle">{{ val.major }} {{ dictObj.edu.find(e => e.value === val.educationType)?.label}}</view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+defineProps({ data: Array })
+import { timesTampChange } from '@/utils/date'
+import { dictObj } from '@/utils/position'
+</script>
+
+<style scoped lang="scss">
+.content {
+  &-item {
+    padding: 20rpx 0;
+  }
+  &-title {
+    display: flex;
+    justify-content: space-between;
+    .name {
+      font-size: 30rpx;
+      color: #333;
+    }
+    .time {
+      color: #999;
+      font-size: 24rpx;
+      display: flex;
+      align-items: center;
+      .icon {
+        margin-left: 20rpx;
+      }
+    }
+  }
+  &-subTitle {
+    font-size: 24rpx;
+    margin-top: 6rpx;
+    color: #999;
+  }
+  &-main {
+    margin-top: 20rpx;
+    font-size: 24rpx;
+    color: #999;
+  }
+}
+</style>

+ 45 - 0
pagesB/personnelDetails/components/jobIntention.vue

@@ -0,0 +1,45 @@
+<template>
+	<view>
+		<view v-for="val in data" :key="val.id" class="ss-m-b-40">
+			<view class="item">
+        <!-- <view class="item-title">{{ val.jobTypeName}}</view> -->
+        <view style="display: flex;justify-content: space-between;align-items: center;">
+          <text class="mr-20 item-title">{{ val.position}}</text>
+          <text class="color-error font-weight-bold">{{ val.payFrom }} {{ val.payFrom  && val.payTo ? '-' : ''}} {{ val.payTo}}</text>
+        </view>
+        <view>{{ val.interestedArea && val.interestedArea.length ? val.workArea + ',' + val.interestedArea.map(e => e.name).join(',') : val.workArea }}</view>
+        <view class="item-tags">
+          <view v-for="industry in val.industry" :key="industry.id" class="tag">{{ industry.nameCn }}</view>
+        </view>
+      </view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+defineProps({ data: Array })
+</script>
+
+<style scoped lang="scss">
+.item {
+  font-size: 28rpx;
+  color: #666;
+  &-title {
+    color: #000;
+    font-weight: 600;
+  }
+  &-tags {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    .tag {
+      border: 2rpx solid #00B760;
+      color: #00B760;
+      padding: 4rpx 16rpx;
+      font-size: 24rpx;
+      margin: 10rpx 10rpx 0 0;
+      border-radius: 10rpx;
+    }
+  }
+}
+</style>

+ 72 - 0
pagesB/personnelDetails/components/trainingExperience.vue

@@ -0,0 +1,72 @@
+<template>
+	<view class="content">
+    <view
+      v-for="val in data"
+      :key="val.id"
+      class="content-item"
+    >
+      <view class="content-title">
+        <view class="name">{{ val.orgName }}</view>
+        <view class="time">
+          {{ timesTampChange(val.startTime ,'Y-M') }} - {{ val.endTime ? timesTampChange(val.endTime ,'Y-M') : '至今' }}
+        </view>
+      </view>
+      <view class="content-subTitle">课程:{{ val.course }}</view>
+      <view class="content-main ellipsis-2">
+				内容:
+				<rich-text v-if="val.content" :nodes="cleanedHtml(val.content)"></rich-text>
+			</view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+defineProps({ data: Array })
+import { timesTampChange } from '@/utils/date'
+
+const cleanedHtml = (text) => {
+  const cleaned = text.replace(/\n/g, '<br>')
+  .replace(/\s+/g, ' ')
+  .replace(/(^|\s+)<\/p>(\s*<p>|$)/g, '</p><p>')
+  .replace(/<p>\s*(<br>)\s*<\/p>/g, '')
+  .replace(/<pre([^>]*)>/g, '<div$1>')
+  .replace(/<\/pre>/g, '</div>')
+  .replace(/<p>\s*(<\/br>)\s*<\/p>/g, '').trim()
+  return cleaned
+}
+</script>
+
+<style scoped lang="scss">
+.content {
+  &-item {
+    padding: 20rpx 0;
+  }
+  &-title {
+    display: flex;
+    justify-content: space-between;
+    .name {
+      font-size: 30rpx;
+      color: #333;
+    }
+    .time {
+      color: #999;
+      font-size: 24rpx;
+      display: flex;
+      align-items: center;
+      .icon {
+        margin-left: 20rpx;
+      }
+    }
+  }
+  &-subTitle {
+    font-size: 24rpx;
+    margin-top: 6rpx;
+    color: #999;
+  }
+  &-main {
+    margin-top: 20rpx;
+    font-size: 24rpx;
+    color: #999;
+  }
+}
+</style>

+ 72 - 0
pagesB/personnelDetails/components/workExp.vue

@@ -0,0 +1,72 @@
+<template>
+	<view class="content">
+    <view
+      v-for="work in data"
+      :key="work.id"
+      class="content-item"
+    >
+      <view class="content-title">
+        <view class="name">{{ work.enterpriseName }}</view>
+        <view class="time">
+          {{ timesTampChange(work.startTime ,'Y-M') }} - {{ work.endTime ? timesTampChange(work.endTime ,'Y-M') : '至今' }}
+        </view>
+      </view>
+      <view class="content-subTitle">{{ work.positionName }}</view>
+      <view class="content-main ellipsis-2">
+				内容:
+				<rich-text v-if="work.content" :nodes="cleanedHtml(work.content)"></rich-text>
+			</view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+defineProps({ data: Array })
+import { timesTampChange } from '@/utils/date'
+
+const cleanedHtml = (text) => {
+  const cleaned = text.replace(/\n/g, '<br>')
+  .replace(/\s+/g, ' ')
+  .replace(/(^|\s+)<\/p>(\s*<p>|$)/g, '</p><p>')
+  .replace(/<p>\s*(<br>)\s*<\/p>/g, '')
+  .replace(/<pre([^>]*)>/g, '<div$1>')
+  .replace(/<\/pre>/g, '</div>')
+  .replace(/<p>\s*(<\/br>)\s*<\/p>/g, '').trim()
+  return cleaned
+}
+</script>
+
+<style scoped lang="scss">
+.content {
+  &-item {
+    padding: 20rpx 0;
+  }
+  &-title {
+    display: flex;
+    justify-content: space-between;
+    .name {
+      font-size: 30rpx;
+      color: #333;
+    }
+    .time {
+      color: #999;
+      font-size: 24rpx;
+      display: flex;
+      align-items: center;
+      .icon {
+        margin-left: 20rpx;
+      }
+    }
+  }
+  &-subTitle {
+    font-size: 24rpx;
+    margin-top: 6rpx;
+    color: #999;
+  }
+  &-main {
+    margin-top: 20rpx;
+    font-size: 24rpx;
+    color: #999;
+  }
+}
+</style>

+ 176 - 0
pagesB/personnelDetails/index.vue

@@ -0,0 +1,176 @@
+<template>
+	<view style="padding: 15px 15px 70px 15px; position: relative;">
+		<baseInfo v-if="cvData?.person" :data="cvData?.person" />
+		<view class="gap-line"></view>
+
+		<view>
+			<view class="title-line">职业技能</view>
+			<view class="tags" v-if="skillExp?.length">
+        <view
+          v-for="skill in skillExp"
+          :key="skill.title"
+          class="tag"
+        >
+          {{ skill.title }}
+        </view>
+      </view>
+			<view v-else class="color-999 text-center">暂无数据</view>
+		</view>
+
+		<view class="gap-line"></view>
+		<view>
+			<view class="title-line">个人优势</view>
+			<advantage v-if="cvData?.person?.advantage" :data="cvData?.person?.advantage" />
+			<view v-else class="color-999 text-center">暂无数据</view>
+		</view>
+
+		<view class="gap-line"></view>
+		<view>
+			<view class="title-line">求职意向</view>
+			<jobIntention v-if="cvData?.interestedList?.length" :data="dealJobData(cvData?.interestedList || [])" />
+			<view v-else class="color-999 text-center">暂无数据</view>
+		</view>
+
+		<view class="gap-line"></view>
+		<view>
+			<view class="title-line">教育经历</view>
+			<eduExp v-if="cvData?.eduList?.length" :data="cvData?.eduList" />
+			<view v-else class="color-999 text-center">暂无数据</view>
+		</view>
+
+		<view class="gap-line"></view>
+		<view>
+			<view class="title-line">工作经历</view>
+			<workExp v-if="cvData?.workList?.length" :data="cvData?.workList" />
+			<view v-else class="color-999 text-center">暂无数据</view>
+		</view>
+
+		<view class="gap-line"></view>
+		<view>
+			<view class="title-line">培训经历</view>
+			<trainingExperience v-if="cvData?.trainList?.length" :data="cvData?.trainList" />
+			<view v-else class="color-999 text-center">暂无数据</view>
+		</view>
+
+		<view class="bottom-actions">
+			操作按钮
+		</view>
+	</view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { dealJobData } from '@/utils/dict'
+import { getDict } from '@/hooks/useDictionaries'
+import { getText } from '@/utils/getText'
+import { dealDictObjData } from '@/utils/position'
+import { getPersonCvDetail } from '@/api/enterprise.js'
+import baseInfo from './components/baseInfo.vue'
+import advantage from './components/advantage.vue'
+import jobIntention from './components/jobIntention.vue'
+import workExp from './components/workExp.vue'
+import eduExp from './components/eduExp.vue'
+import trainingExperience from './components/trainingExperience.vue'
+
+const cvData = ref({})
+
+const skillExp = ref([])
+const getSkillExp = async (data) => {
+	if (!data || !data.length) {
+    return
+  }
+
+	const { data: _skillList} = await getDict('skillList', {}, 'skillList')
+  const skillList = _skillList?.data
+  if (!skillList || !skillList.length) {
+    return
+  }
+
+	const { data: _skillLevelArr } = await getDict('menduner_skill_level')
+  const skillLevelArr = _skillLevelArr?.data
+  if (!skillLevelArr || !skillLevelArr.length) {
+    return
+  }
+
+	skillExp.value = data.map(e => {
+    return {
+      ...e,
+      title: `${getText(e.skillId, skillList, 'nameCn', 'id')} / ${getText(e.level, skillLevelArr)}`
+    }
+  })
+}
+
+const getDetail = async (id) => {
+	uni.showLoading({ title: '加载中' })
+	try {
+		const { data } = await getPersonCvDetail(id)
+		if (!data) return
+		data.person = dealDictObjData({}, data.person)
+		cvData.value = data || {}
+		getSkillExp(data?.skillList || [])
+		uni.hideLoading()
+	} catch {
+		uni.hideLoading()
+	}
+}
+
+onLoad(async (options) => {
+	const { id } = options
+	if (!id) {
+		uni.showToast({
+			title: '缺少人员id',
+			icon: 'none'
+		})
+		setTimeout(() => {
+			uni.navigateBack({ delta: 1 })
+		}, 1000)
+		return
+	}
+	await getDetail(id)
+})
+</script>
+
+<style scoped lang="scss">
+.gap-line {
+	border-bottom: 1px solid #eee;
+	margin: 30px 0;
+}
+.title-line {
+	font-size: 20px;
+	position: relative;
+	line-height: 20px;
+	margin-left: 12px;
+	margin-bottom: 20px;
+	padding-left: 10px;
+	&::before {
+		content: '';
+		position: absolute;
+		width: 6px;
+		height: 20px;
+		background: #00B760;
+		left: -10px;
+		border-radius: 6px;
+	}
+}
+.tags {
+  display: flex;
+  flex-wrap: wrap;
+  .tag {
+    margin: 0 10rpx 10rpx 0;
+    border: 2rpx solid #00B760;
+    color: #00B760;
+    white-space: nowrap;
+    padding: 4rpx 10rpx;
+    border-radius: 10rpx;
+    font-size: 24rpx;
+  }
+}
+.bottom-actions {
+	position: fixed;
+	width: 100%;
+	bottom: 0;
+	height: 50px;
+	background-color: #fff;
+}
+</style>

+ 94 - 0
utils/dict.js

@@ -0,0 +1,94 @@
+import { reactive } from 'vue'
+import { getDict } from '@/hooks/useDictionaries'
+
+const dictObj = reactive({})
+const dictList = [
+  { 
+    type: 'positionData', 
+    apiType: 'positionData', 
+    key: 'positionId', 
+    label: 'position', 
+    value: 'positionTypeData', 
+    itemKey: 'id', 
+    itemText: 'nameCn'
+  },
+  { 
+    type: 'industryList', 
+    apiType: 'industryList', 
+    key: 'industryIdList', 
+    label: 'industry', 
+    isArray: true, 
+    value: 'industryTypeData', 
+    itemKey: 'id', 
+    itemText: 'nameCn' 
+  },
+  { 
+    type: 'menduner_area_type',
+    apiType: 'areaList',
+    key: 'workAreaId',
+    params: { type: undefined },
+    label: 'workArea',
+    value: 'areaTypeData',
+    itemKey: 'id',
+    itemText: 'name'
+  },
+  { 
+    type: 'menduner_job_type',
+    key: 'jobType',
+    label: 'jobTypeName',
+    value: 'jobTypeData',
+    itemKey: 'value',
+    itemText: 'label'
+  },
+  { 
+    type: 'menduner_area_type', 
+    apiType: 'areaList', 
+    key: 'interestedAreaIdList', 
+    label: 'interestedArea', 
+    // params: { type: undefined },
+    isArray: true, 
+    value: 'areaTypeData', 
+    itemKey: 'id', 
+    itemText: 'name' 
+  }
+]
+
+// 字典
+const getDictList = async () => {
+  dictList.forEach(async (val) => {
+    const { data } = await getDict(val.type, val.params, val.apiType)
+    if (!data?.data) {
+      dictObj[val.value] = []
+      return
+    }
+    dictObj[val.value] = data.data
+  })
+}
+
+const getData = async () => {
+  await getDictList()
+}
+getData()
+
+export const dealJobData = (list) => {
+  let res = {}
+  dictList.forEach(item => {
+    res = list.map(e => {
+      let obj = {}
+      if (item.isArray) {
+        if (e[item.key] && e[item.key].length) {
+          const result = e[item.key].map(val => {
+            return obj = dictObj[item.value].find(i => Number(i[item.itemKey]) === Number(val))
+          })
+          e[item.label] = result && result.length ? result.filter(Boolean) : []
+        }
+      } else {
+        obj = dictObj[item.value].find(k => Number(k[item.itemKey]) === Number(e[item.key]))
+        if (!obj) return
+        e[item.label] = obj[item.itemText]
+      }
+      return e
+    })
+  })
+  return res
+}