Files
ks-app-employment-service/packageA/pages/post/post.vue
2025-12-04 17:18:45 +08:00

971 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<AppLayout backGorundColor="#F4F4F4">
<template #headerleft>
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
<template #headerright>
<!-- <view class="btnshare">
<image src="@/static/icon/share.png" @click="shareJob"></image>
</view> -->
<view class="btn mar_ri10">
<image src="@/static/icon/collect3.png" v-if="jobInfo.isCollection!==0" @click="jobCollection"></image>
<image src="@/static/icon/collect2.png" v-else @click="jobCollection"></image>
</view>
</template>
<view class="content" v-show="!isEmptyObject(jobInfo)">
<view class="content-top btn-feel">
<view style="background: #ffffff;padding: 24rpx;box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);border-radius: 20rpx 20rpx 20rpx 20rpx;position: relative;overflow: hidden;">
<view class="top-salary">
<Salary-Expectation
:max-salary="jobInfo.maxSalary"
:min-salary="jobInfo.minSalary"
:is-month="true"
></Salary-Expectation>
</view>
<view class="top-name">{{ jobInfo.jobTitle }}</view>
<view class="top-info">
<view class="info-img"><image src="/static/icon/post12.png"></image></view>
<view class="info-text">
<dict-Label dictType="experience" :value="jobInfo.experience"></dict-Label>
</view>
<view class="info-img mar_le20"><image src="/static/icon/post13.png"></image></view>
<view class="info-text">
<dict-Label dictType="education" :value="jobInfo.education"></dict-Label>
</view>
</view>
<!-- <view class="position-source">
<text>来源&nbsp;</text>
{{ jobInfo.dataSource }}
</view> -->
<view class="position-source">
<view class="btn">
<image src="@/static/icon/collect3.png" v-if="jobInfo.isCollection!==0" @click="jobCollection"></image>
<image src="@/static/icon/collect2.png" v-else @click="jobCollection"></image>
</view>
</view>
<view class="publish-time" v-if="jobInfo.postingDate">
{{ formatPublishTime(jobInfo.postingDate) }}
</view>
</view>
</view>
<view class="ai-explain" v-if="jobInfo.isExplain">
<view class="exbg">
<view class="explain-left btn-shaky">
<view class="leftText">AI+智能讲解职位一听就懂</view>
<view class="leftdownText">懒得看文字我用AI讲给你听</view>
</view>
<view class="explain-right button-click" @click="seeExplain">点击查看</view>
</view>
</view>
<view class="content-card">
<view class="card-title">
<text class="title">职位描述</text>
</view>
<view class="description" :style="{ whiteSpace: 'pre-wrap' }">
{{ jobInfo.description }}
</view>
</view>
<!-- 职位图片 -->
<view class="content-card" v-if="jobInfo.filesList && jobInfo.filesList.length > 0">
<view class="card-title">
<text class="title">职位图片</text>
</view>
<view class="job-images">
<view class="image-item" v-for="(file, index) in jobInfo.filesList" :key="file.id">
<image :src="file.fileUrl" mode="aspectFit" @click="previewImage(file.fileUrl, index)"></image>
</view>
</view>
</view>
<!-- 联系人信息 -->
<view class="content-card" v-if="jobInfo.jobContactList && jobInfo.jobContactList.length > 0">
<view class="card-title">
<text class="title">联系人信息</text>
</view>
<view class="contact-list">
<view class="contact-item" v-for="(contact, index) in jobInfo.jobContactList" :key="index">
<view class="contact-info">
<view class="contact-label">联系人</view>
<view class="contact-value">{{ contact.contactPerson }}</view>
</view>
<view class="contact-info">
<view class="contact-label">职位</view>
<view class="contact-value">{{ contact.position }}</view>
</view>
<view class="contact-info">
<view class="contact-label">电话</view>
<view class="contact-value">{{ contact.contactPersonPhone }}</view>
</view>
</view>
</view>
</view>
<!-- 公司信息 -->
<view class="content-card">
<view class="card-title">
<text class="title">公司信息</text>
<text
class="btntext button-click"
@click="navTo(`/packageA/pages/UnitDetails/UnitDetails?companyId=${jobInfo.company.companyId}`)"
>
单位详情
</text>
</view>
<view class="company-info">
<view class="companyinfo-left">
<image src="@/static/icon/companyIcon.png" mode=""></image>
</view>
<view class="companyinfo-right">
<view class="row1">{{ jobInfo.company?.name }}</view>
<view class="row2">
<dict-tree-Label
v-if="jobInfo.company?.industry"
dictType="industry"
:value="jobInfo.company?.industry"
></dict-tree-Label>
<span v-if="jobInfo.company?.industry">&nbsp;</span>
<dict-Label dictType="scale" :value="jobInfo.company?.scale"></dict-Label>
</view>
<view class="row2">
<text>在招</text>
<text style="color: #256bfa">{{ companyCount }}</text>
<text>个职位</text>
</view>
</view>
</view>
<view class="company-map" v-if="jobInfo.latitude && jobInfo.longitude">
<map
style="width: 100%; height: 100%"
:latitude="jobInfo.latitude"
:longitude="jobInfo.longitude"
:markers="mapCovers"
></map>
</view>
</view>
<view class="content-card" v-if="currentUserType !== 0">
<view class="card-title">
<text class="title">竞争力分析</text>
</view>
<view class="description">
三个月内共15位求职者申请你的简历匹配度为{{ raderData.matchScore }}排名位于第{{
raderData.rank
}}超过{{ raderData.percentile }}%的竞争者处在优秀位置
</view>
<RadarMap :value="raderData"></RadarMap>
<view class="card-footer">
<view class="footer-title">你与职位的匹配度</view>
<view class="footer-content">
<view class="progress-container">
<view
v-for="(item, index) in matchingDegree"
:key="index"
class="progress-item"
:class="getClass(index)"
/>
</view>
</view>
<view class="progress-text">
<view class="text-rpx" v-for="(item, index) in matchingDegree" :key="index">{{ item }}</view>
</view>
</view>
</view>
<view class="content-card" v-else>
<view class="card-title">
<view class="title">申请人列表</view>
</view>
<view class="applicant-list">
<view v-for="applicant in applicants" :key="applicant.userId" class="applicant-item">
<view class="item-header">
<view class="name">{{ applicant.name }}</view>
<view class="right-header">
<view class="matching-degree">匹配度{{ applicant.matchingDegree }}</view>
<button class="resume-button" @click="viewResume(applicant.userId)">查看简历</button>
</view>
</view>
<view class="item-details">
<view class="detail-text">
<view class="label">年龄{{ applicant.age }}</view>
</view>
<view class="detail-text">
<view class="label">
学历
<dict-Label dictType="education" :value="applicant.education"></dict-Label>
</view>
</view>
<view class="detail-text">
<view class="label">
经验
<dict-Label dictType="experience" :value="applicant.experience"></dict-Label>
</view>
</view>
<view class="detail-text">
<view class="label">
期望薪资
<Salary-Expectation
style="display: inline-block"
:max-salary="applicant.maxSalary"
:min-salary="applicant.minSalary"
:is-month="true"
></Salary-Expectation>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<view style="height: 34px"></view>
<template #footer>
<view class="footer">
<view class="btn-wq button-click" @click="jobApply">投递简历</view>
</view>
</template>
<VideoPlayer ref="videoPalyerRef" />
</AppLayout>
</template>
<script setup>
import point from '@/static/icon/point.png';
import VideoPlayer from './component/videoPlayer.vue';
import { reactive, inject, watch, ref, onMounted, computed } from 'vue';
import { onLoad, onShow, onHide } from '@dcloudio/uni-app';
import dictLabel from '@/components/dict-Label/dict-Label.vue';
import RadarMap from './component/radarMap.vue';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { userInfo } = storeToRefs(useUserStore());
// 与首页一致的用户类型获取优先store兜底缓存
const currentUserType = computed(() => {
const storeIsCompanyUser = userInfo.value?.isCompanyUser;
const cachedIsCompanyUser = (uni.getStorageSync('userInfo') || {}).isCompanyUser;
return Number(storeIsCompanyUser !== undefined ? storeIsCompanyUser : cachedIsCompanyUser);
});
const { $api, navTo, getLenPx, parseQueryParams, navBack, isEmptyObject } = inject('globalFunction');
import config from '@/config.js';
const matchingDegree = ref(['一般', '良好', '优秀', '极好']);
const currentStep = ref(1);
const companyCount = ref(0);
const jobInfo = ref({});
const state = reactive({});
const mapCovers = ref([]);
const jobIdRef = ref();
// 竞争力分析数据,初始化为包含默认值的完整结构,确保雷达图能正常渲染
const raderData = ref({
matchScore: 0,
rank: 0,
percentile: 0,
radarChart: {
skill: 0,
experience: 0,
education: 0,
salary: 0,
age: 0,
location: 0
}
});
const videoPalyerRef = ref(null);
const explainUrlRef = ref('');
const applicants = ref([
{
createTime: null,
userId: 1,
name: '青岛测试账号331',
age: '28', // 假设年龄有值
sex: '1',
birthDate: null,
education: '4',
politicalAffiliation: '',
phone: '',
avatar: '',
salaryMin: '10000',
salaryMax: '15000',
area: '3',
status: '0',
loginIp: '',
loginDate: null,
jobTitleId: '157,233,373',
experience: '3',
isRecommend: 1,
jobTitle: ['人力资源专员/助理', 'Java', '运维工程师'],
applyDate: '2025-09-26',
matchingDegree: 1,
},
]);
onLoad((option) => {
console.log(option, 'option');
if (option.jobId) {
initLoad(option);
}
});
onShow(() => {
// 仅在 H5 环境中从 URL 获取参数(小程序环境中 onShow 不会传递 URL 参数)
// #ifdef H5
try {
const option = parseQueryParams(); // 兼容微信内置浏览器
if (option.jobId) {
initLoad(option);
}
} catch (e) {
console.warn('onShow 中解析 URL 参数失败:', e);
}
// #endif
});
function initLoad(option) {
const jobId = decodeURIComponent(option.jobId);
if (jobId !== jobIdRef.value) {
jobIdRef.value = jobId;
getDetail(jobId);
}
}
function seeExplain() {
if (jobInfo.value.explainUrl) {
videoPalyerRef.value?.open(jobInfo.value.explainUrl);
// console.log(jobInfo.value.explainUrl);
// explainUrlRef.value = jobInfo.value.explainUrl;
}
}
function getDetail(jobId) {
return new Promise((reslove, reject) => {
$api.createRequest(`/app/job/${jobId}`).then((resData) => {
const { latitude, longitude, companyName, companyId } = resData.data;
jobInfo.value = resData.data;
reslove(resData.data);
getCompanyIsAJobs(companyId);
if (currentUserType.value !== 0) {
getCompetivetuveness(jobId);
}
// getCompetivetuveness(jobId);
if (latitude && longitude) {
mapCovers.value = [
{
latitude: latitude,
longitude: longitude,
iconPath: point,
label: {
content: companyName,
textAlign: 'center',
padding: 3,
fontSize: 12,
bgColor: '#FFFFFF',
anchorX: getTextWidth(companyName), // X 轴调整,负数向左
borderRadius: 5,
},
width: 34,
},
];
}
});
});
}
function getCompanyIsAJobs(companyId) {
$api.createRequest(`/app/company/count/${companyId}`).then((resData) => {
companyCount.value = resData.data;
});
}
function getTextWidth(text, size = 12) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = `${12}px Arial`;
return -(context.measureText(text).width / 2) - 20; // 计算文字中心点
}
function getCompetivetuveness(jobId) {
$api.createRequest(`/app/job/competitiveness/${jobId}`, {}, 'GET').then((resData) => {
// 如果接口返回的数据为 null 或空使用默认值0
if (resData && resData.data) {
// 确保 radarChart 字段存在,如果不存在则使用默认值
const radarChart = resData.data.radarChart || {
skill: 0,
experience: 0,
education: 0,
salary: 0,
age: 0,
location: 0
};
raderData.value = {
matchScore: resData.data.matchScore || 0,
rank: resData.data.rank || 0,
percentile: resData.data.percentile || 0,
radarChart: radarChart
};
currentStep.value = (resData.data.matchScore || 0) * 0.04;
} else {
// 接口返回 null 或空数据时使用默认值0
raderData.value = {
matchScore: 0,
rank: 0,
percentile: 0,
radarChart: {
skill: 0,
experience: 0,
education: 0,
salary: 0,
age: 0,
location: 0
}
};
currentStep.value = 0;
}
}).catch((error) => {
// 接口请求失败时使用默认值0
console.error('获取竞争力分析失败:', error);
raderData.value = {
matchScore: 0,
rank: 0,
percentile: 0,
radarChart: {
skill: 0,
experience: 0,
education: 0,
salary: 0,
age: 0,
location: 0
}
};
currentStep.value = 0;
});
}
// 申请岗位
function jobApply() {
const jobId = jobInfo.value.jobId;
$api.createRequest(`/app/job/apply/${jobId}`, {}, 'GET').then((resData) => {
getDetail(jobId);
$api.msg('申请成功');
const jobUrl = jobInfo.value.jobUrl;
// return window.open(jobUrl);
});
// if (jobInfo.value.isApply) {
// const jobUrl = jobInfo.value.jobUrl;
// return window.open(jobUrl);
// } else {
// $api.createRequest(`/app/job/apply/${jobId}`, {}, 'GET').then((resData) => {
// getDetail(jobId);
// $api.msg('申请成功');
// const jobUrl = jobInfo.value.jobUrl;
// return window.open(jobUrl);
// });
// }
}
// 取消/收藏岗位
function jobCollection() {
const jobId = jobInfo.value.jobId;
if (jobInfo.value.isCollection) {
$api.createRequest(`/app/job/collection/${jobId}`, {}, 'DELETE').then((resData) => {
getDetail(jobId);
$api.msg('取消收藏成功');
});
} else {
$api.createRequest(`/app/job/collection/${jobId}`, {}, 'POST').then((resData) => {
getDetail(jobId);
$api.msg('收藏成功');
});
}
}
function getClass(index) {
const current = currentStep.value;
const floorIndex = Math.floor(current);
if (index < floorIndex) {
return 'active';
} else if (index === floorIndex) {
const decimal = current % 1;
const percent = Math.round(decimal * 100);
return `half${percent}`;
} else {
return '';
}
}
// 格式化发布时间
function formatPublishTime(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const now = new Date();
const diffTime = Math.abs(now - date);
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return '今天发布';
} else if (diffDays === 1) {
return '昨天发布';
} else if (diffDays < 7) {
return `${diffDays}天前发布`;
} else if (diffDays < 30) {
const weeks = Math.floor(diffDays / 7);
return `${weeks}周前发布`;
} else {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
}
}
// 预览图片
function previewImage(url, index) {
// 获取所有图片的URL列表
const allImageUrls = jobInfo.value.filesList.map(file => file.fileUrl);
uni.previewImage({
urls: allImageUrls,
current: index
});
}
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 52rpx;
height: 52rpx;
}
.btnshare {
width: 48rpx;
height: 48rpx;
margin-right: 46rpx;
}
image {
height: 100%;
width: 100%;
}
.progress-container {
display: flex;
align-items: center;
gap: 8rpx; /* 间距 */
margin-top: 24rpx
}
.progress-text{
margin-top: 8rpx
display: flex;
align-items: center;
gap: 8rpx; /* 间距 */
justify-content: space-around
width: 100%
font-weight: 400;
font-size: 20rpx;
color: #999999;
text-align: center;
}
.progress-item {
width: 25%;
height: 24rpx;
background-color: #eee;
border-radius: 24rpx;
overflow: hidden;
position: relative;
transition: background-color 0.3s;
}
/* 完整激活格子 */
.progress-item.active {
background: linear-gradient(to right, #256bfa, #8c68ff);
}
/* 当前进度进行中的格子 */
for i in 0..100
.progress-item.half{i}::before
content ''
position absolute
left 0
top 0
bottom 0
width 100%
background linear-gradient(to right, #256bfa (i)%, #eaeaea (i)%)
border-radius 24rpx
.card-footer{
.footer-title{
font-weight: 600;
font-size: 28rpx;
color: #000000;
}
.footer-content{
.content-line{
display: grid
grid-template-columns: repeat(4, 1fr)
background: linear-gradient( to left, #9E74FD 0%, #256BFA 100%);
border-radius: 10rpx
.line-pargrah{
height: 20rpx;
position: relative
}
.line-pargrah::after{
position: absolute;
content: '';
right: 10
top: 0
width: 6rpx
height: 20rpx
background: #FFFFFF
}
}
}
}
// 职位图片样式
.job-images{
margin-top: 30rpx
display: flex
flex-wrap: wrap
gap: 20rpx
.image-item{
width: calc(50% - 10rpx)
height: 300rpx
border-radius: 12rpx
overflow: hidden
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1)
image{
width: 100%
height: 100%
object-fit: cover
}
}
}
// 联系人信息样式
.contact-list{
margin-top: 30rpx
.contact-item{
padding: 20rpx
background-color: #f8f9fa
border-radius: 12rpx
.contact-info{
display: flex
align-items: center
margin-bottom: 15rpx
&:last-child{
margin-bottom: 0
}
.contact-label{
font-weight: 500
font-size: 28rpx
color: #333333
width: 120rpx
}
.contact-value{
font-weight: 400
font-size: 28rpx
color: #495265
}
}
}
}
// ai
.ai-explain{
margin-top: 28rpx
background-color: #FFFFFF
border-radius: 20rpx 20rpx 20rpx 20rpx;
overflow: hidden
.exbg{
padding: 28rpx 40rpx;
display: flex
align-items: center
justify-content: space-between
background: url('@/static/icon/aibg.png') center center no-repeat;
background-size: 100% 100%
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
.explain-left{
.leftText{
font-weight: 600;
font-size: 32rpx;
}
.leftdownText{
margin-top: 12rpx
font-weight: 400;
font-size: 28rpx;
color: #495265;
}
}
.explain-right{
width: 168rpx;
height: 72rpx;
border-radius: 40rpx 40rpx 40rpx 40rpx;
border: 2rpx solid #E7E9ED;
text-align: center;
line-height: 72rpx
font-weight: 400;
font-size: 28rpx;
color: #333333;
}
}
.content{
padding: 0 28rpx
height: 100%
padding-top: 28rpx
.content-top{
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);
border-radius: 20rpx 20rpx 20rpx 20rpx;
padding: 24rpx
position: relative
overflow: hidden
.top-salary{
font-weight: 500;
font-size: 32rpx;
color: #4C6EFB;
}
.top-name{
font-weight: 600;
font-size: 36rpx;
color: #000000;
margin-top: 8rpx
}
.top-info{
margin-top: 22rpx
display: flex;
align-items: center
.info-img{
width: 40rpx;
height: 40rpx
}
.info-text{
font-weight: 400;
font-size: 28rpx;
color: #6C7282;
margin-left: 10rpx
}
}
.position-source{
position: absolute
top: 0
right: 0
width: fit-content;
height: 65rpx;
padding: 0 24rpx
background: rgba(37,107,250,0.1);
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);
text-align: center
line-height: 76rpx
font-weight: 400;
font-size: 28rpx;
color: #64779F;
border-bottom-left-radius: 20rpx
}
}
.content-card{
padding: 24rpx
margin-top: 28rpx
background: #FFFFFF;
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);
border-radius: 20rpx 20rpx 20rpx 20rpx;
.card-title{
font-weight: 600;
font-size: 32rpx;
color: #000000;
position: relative;
display: flex
justify-content: space-between
.title{
position: relative;
z-index: 2;
}
.btntext{
white-space: nowrap
font-weight: 400;
font-size: 28rpx;
color: #256BFA;
}
}
.card-title::before{
position: absolute
content: '';
left: -14rpx
bottom: 0
height: 16rpx;
width: 108rpx;
background: linear-gradient(to right, #CBDEFF, #FFFFFF);
border-radius: 8rpx;
z-index: 1;
}
.description{
margin-top: 30rpx
font-weight: 400;
font-size: 28rpx;
color: #495265;
}
.company-info{
padding-top: 30rpx
display: flex
flex-direction: row
flex-wrap: nowrap
.companyinfo-left{
width: 96rpx;
height: 96rpx;
margin-right: 24rpx
}
.companyinfo-right{
.row1{
font-weight: 500;
font-size: 32rpx;
color: #333333;
}
.row2{
font-weight: 400;
font-size: 28rpx;
color: #6C7282;
line-height: 45rpx;
}
}
}
.company-map{
margin-top: 28rpx
height: 416rpx;
border-radius: 20rpx 20rpx 20rpx 20rpx;
overflow: hidden
}
}
}
/* #ifdef H5 */
.footer{
position: fixed;
bottom: 0;
width: 100%;
padding: 20rpx 0!important
z-index: 1000;
.btn-wq{
display: block;
width: 94%;
margin: 0 auto;
}
}
/* #endif */
.footer{
background: #FFFFFF;
box-shadow: 0rpx -4rpx 24rpx 0rpx rgba(11,44,112,0.12);
border-radius: 0rpx 0rpx 0rpx 0rpx;
padding: 40rpx 28rpx 20rpx 28rpx
.btn-wq{
height: 90rpx;
background: #256BFA;
border-radius: 12rpx 12rpx 12rpx 12rpx;
font-weight: 500;
font-size: 32rpx;
color: #FFFFFF;
text-align: center;
line-height: 90rpx
}
}
.content-card {
background-color: #fff;
border-radius: 8px;
margin: 10px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-title {
padding-bottom: 10px;
border-bottom: 1px solid #eee;
margin-bottom: 10px;
}
.title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.applicant-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.applicant-item {
padding: 15px;
background-color: #fafafa;
border-radius: 8px;
border: 1px solid #f0f0f0;
}
.item-header {
display: flex;
justify-content: space-between; /* 名字和右侧部分分开 */
align-items: center;
margin-bottom: 10px;
}
.right-header {
display: flex;
align-items: center;
gap: 10px;
}
.name {
font-size: 16px;
font-weight: bold;
max-width: 100px; /* 限制名字的最大宽度,根据你的布局调整 */
overflow: hidden; /* 隐藏超出部分 */
white-space: nowrap; /* 不换行 */
text-overflow: ellipsis; /* 显示省略号 */
}
.matching-degree {
font-size: 14px;
color: #4CAF50;
font-weight: 500;
}
.resume-button {
font-size: 12px;
padding: 5px 10px;
border-radius: 20px;
border: 1px solid #007aff;
color: #007aff;
background-color: transparent;
line-height: 1;
height: auto;
}
.resume-button::after {
border: none;
}
.item-details {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.detail-text {
font-size: 14px;
color: #666;
/* 确保标签和值在同一行 */
display: flex;
white-space: nowrap;
}
.label {
font-weight: bold;
color: #333;
}
</style>