job-detail.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736
  1. <template>
  2. <view class="job-detail-container">
  3. <!-- 顶部信息区域 -->
  4. <view class="job-header">
  5. <view class="job-title">{{ jobDetail.title }}</view>
  6. <view class="job-salary">{{ jobDetail.salary }}</view>
  7. <view class="job-department">{{ jobDetail.department }}</view>
  8. <view class="job-requirements">
  9. <view class="requirement-item">
  10. <view class="dot"></view>
  11. <text style="width: 100%;">{{ formatLocation(jobDetail.location) }}</text>
  12. </view>
  13. <view class="requirement-item">
  14. <view class="time-icon"></view>
  15. <text>{{ jobDetail.experience }}</text>
  16. </view>
  17. </view>
  18. </view>
  19. <!-- 工作地点区域 -->
  20. <view class="section">
  21. <view class="section-title">工作地点</view>
  22. <view class="map-container">
  23. <map
  24. id="jobLocationMap"
  25. class="map"
  26. :latitude="mapInfo.latitude"
  27. :longitude="mapInfo.longitude"
  28. :markers="mapInfo.markers"
  29. :scale="16"
  30. @tap="openLocation"
  31. ></map>
  32. <!-- <view class="location-text">{{ jobDetail.location }}</view> -->
  33. </view>
  34. </view>
  35. <!-- 福利待遇区域 -->
  36. <!-- <view class="section">
  37. <view class="section-title">福利待遇</view>
  38. <view class="benefits-list">
  39. <view class="benefit-tag" >暂无数据</view>
  40. </view>
  41. </view> -->
  42. <!-- 岗位介绍区域 -->
  43. <view class="section">
  44. <view class="section-title">岗位介绍</view>
  45. <view class="job-description">
  46. <!-- <view class="description-subtitle">{{ jobDetail.description[0].subtitle }}</view> -->
  47. <view class="description-content">
  48. <view class="description-item" v-for="(item, index) in jobDetail.description[0].items" :key="index">
  49. <view class="blue-dot"></view>
  50. <template v-if="hasHtmlTags(item)">
  51. <view class="description-text" v-html="item"></view>
  52. </template>
  53. <template v-else>
  54. <text style="color: #333;">{{ item }}</text>
  55. </template>
  56. </view>
  57. </view>
  58. </view>
  59. </view>
  60. <!-- 右侧开始面试按钮 -->
  61. <view class="interview-button" @click="startInterview">
  62. <text>开始面试</text>
  63. </view>
  64. </view>
  65. </template>
  66. <script>
  67. import { apiBaseUrl } from '@/common/config.js';
  68. import { applyJob } from '@/api/user.js';
  69. export default {
  70. data() {
  71. return {
  72. jobDetail: {
  73. title: '',
  74. salary: '',
  75. department: '',
  76. location: '',
  77. experience: '',
  78. benefits: [],
  79. detailed_address:'',
  80. description: [
  81. {
  82. subtitle: '岗位要求',
  83. items: []
  84. }
  85. ]
  86. },
  87. selectedJobId: null,
  88. jobId: null,
  89. mapInfo: {
  90. latitude: 0,
  91. longitude: 0,
  92. markers: []
  93. },
  94. tenant_id: '', // 租户ID
  95. }
  96. },
  97. onLoad(options) {
  98. this.jobId = options.id;
  99. this.getJobDetail(options.id);
  100. this.getTenantId();
  101. // 尝试从本地存储获取职位详情
  102. try {
  103. const jobDetailStr = uni.getStorageSync('currentJobDetail');
  104. if (jobDetailStr) {
  105. const jobData = JSON.parse(jobDetailStr);
  106. // 这里可以根据实际数据结构进行处理
  107. // 如果后端返回的数据结构与页面需要的不一致,可以在这里进行转换
  108. this.jobDetail = {
  109. ...this.jobDetail,
  110. title: jobData.title || this.jobDetail.title,
  111. salary: jobData.salary || this.jobDetail.salary,
  112. department: jobData.department || this.jobDetail.department,
  113. location: jobData.location || this.jobDetail.location,
  114. experience: jobData.experience || this.jobDetail.experience,
  115. benefits: jobData.benefits || this.jobDetail.benefits,
  116. description: jobData.description || this.jobDetail.description,
  117. detailed_address:jobData.detailed_address || this.jobDetail.detailed_address
  118. };
  119. }
  120. } catch (e) {
  121. console.error('获取职位详情失败:', e);
  122. }
  123. },
  124. methods: {
  125. // 获取本地存储的tenant_id
  126. getTenantId() {
  127. const tenantId = uni.getStorageSync('tenant_id');
  128. if (tenantId) {
  129. this.tenant_id = tenantId;
  130. return tenantId;
  131. }
  132. return null;
  133. },
  134. async getJobDetail(jobId) {
  135. try {
  136. const { data } = await uni.request({
  137. url: `${apiBaseUrl}/api/mini/job/detail?id=${jobId}&tenant_id=${this.tenant_id||JSON.parse(uni.getStorageSync('userInfo')).tenant_id ||1}`,
  138. method: 'GET',
  139. });
  140. if (data.code === 2000) {
  141. // 处理富文本description
  142. let descriptionItems = [];
  143. if (data.data.description) {
  144. // 使用正则表达式提取<li>标签中的文本内容
  145. const liRegex = /<li[^>]*>(.*?)<\/li>/g;
  146. const stripTagsRegex = /<[^>]*>/g;
  147. let match;
  148. while ((match = liRegex.exec(data.data.description)) !== null) {
  149. // 移除剩余的HTML标签,并清理空白字符
  150. const text = match[1]
  151. .replace(stripTagsRegex, '')
  152. .replace(/&nbsp;/g, ' ')
  153. .trim();
  154. if (text) {
  155. descriptionItems.push(text);
  156. }
  157. }
  158. }
  159. // 如果requirements存在,添加到描述项前面
  160. if (data.data.requirements) {
  161. descriptionItems.unshift(data.data.requirements);
  162. }
  163. this.jobDetail = {
  164. id: data.data.id,
  165. title: data.data.title || '',
  166. salary: data.data.salary_range ? `${data.data.salary_range}/月` : '',
  167. department: data.data.job_type_name,//`${data.data.department || ''} ${data.data.job_type === 1 ? '全职' : '兼职'}`,
  168. location: data.data.location || '',
  169. experience: data.data.work_experience_required || '不限',
  170. benefits: data.data.competency_tags || ['五险一金', '带薪年假', '定期体检'],
  171. detailed_address:data.data.detailed_address || '',
  172. description: [
  173. {
  174. subtitle: '岗位要求',
  175. items: descriptionItems
  176. }
  177. ]
  178. };
  179. // 获取职位详情后,更新地图信息
  180. this.updateMapLocation(data.data.location,data.data.detailed_address);
  181. } else {
  182. uni.showToast({
  183. title: '获取职位详情失败',
  184. icon: 'none'
  185. });
  186. }
  187. } catch (e) {
  188. console.error('获取职位详情失败:', e);
  189. uni.showToast({
  190. title: '获取职位详情失败',
  191. icon: 'none'
  192. });
  193. }
  194. },
  195. checkLogin() {
  196. const userInfo = uni.getStorageSync('userInfo');
  197. if (!userInfo) {
  198. uni.showToast({
  199. title: '请先登录',
  200. icon: 'none'
  201. });
  202. return false;
  203. }
  204. return true;
  205. },
  206. // 获取当前职位配置
  207. /* getConfig(selectedJobId){
  208. uni.request({
  209. url: `${apiBaseUrl}/api/job/config/position/${this.jobId}`,
  210. method: 'GET',
  211. data: {
  212. openid: JSON.parse(uni.getStorageSync('userInfo')).openid
  213. },
  214. header: {
  215. 'content-type': 'application/x-www-form-urlencoded'
  216. },
  217. success: (res) => {
  218. // 身份验证成功后,继续提交用户信息
  219. console.log(res);
  220. if (res.statusCode === 200) {
  221. if (res.data.code === 2000) {
  222. uni.setStorageSync('configData', JSON.stringify(res.data.data))
  223. uni.navigateTo({
  224. url: '/pages/Personal/Personal',
  225. fail: (err) => {
  226. console.error('页面跳转失败:', err);
  227. uni.showToast({
  228. title: '页面跳转失败',
  229. icon: 'none'
  230. });
  231. }
  232. });
  233. } else {
  234. }
  235. } else {
  236. uni.hideLoading();
  237. }
  238. },
  239. fail: (err) => {
  240. uni.hideLoading();
  241. uni.showToast({
  242. title: '网络错误,请稍后重试',
  243. icon: 'none'
  244. });
  245. }
  246. });
  247. }, */
  248. // 处理用户信息获取和页面跳转逻辑
  249. handleUserInfoAndNavigation() {
  250. // 调用获取用户完整信息的接口
  251. const openid = JSON.parse(uni.getStorageSync('userInfo')).openid;
  252. uni.request({
  253. url: `${apiBaseUrl}/api/wechat/user/get_full_info?tenant_id=${this.tenant_id||JSON.parse(uni.getStorageSync('userInfo')).tenant_id ||1}&openid=${openid}`,
  254. method: 'GET',
  255. success: (infoRes) => {
  256. if (infoRes.statusCode === 200 && infoRes.data && infoRes.data.data && infoRes.data.data.profile) {
  257. const resumeUrl = infoRes.data.data.profile.resume_url || '';
  258. if (!resumeUrl) {
  259. // resume_url 为空,跳转到其他页面
  260. uni.navigateTo({
  261. url: '/pages/uploadResume/uploadResume', // 假设跳转到上传简历页面
  262. fail: (err) => {
  263. console.error('页面跳转失败:', err);
  264. uni.showToast({
  265. title: '页面跳转失败',
  266. icon: 'none'
  267. });
  268. }
  269. });
  270. } else {
  271. // resume_url 不为空,跳转到 Personal 页面
  272. uni.navigateTo({
  273. url: '/pages/Personal/Personal',
  274. fail: (err) => {
  275. console.error('页面跳转失败:', err);
  276. uni.showToast({
  277. title: '页面跳转失败',
  278. icon: 'none'
  279. });
  280. }
  281. });
  282. }
  283. } else {
  284. // 获取用户信息失败,直接跳转到 Personal 页面
  285. uni.navigateTo({
  286. url: '/pages/Personal/Personal',
  287. fail: (err) => {
  288. console.error('页面跳转失败:', err);
  289. uni.showToast({
  290. title: '页面跳转失败',
  291. icon: 'none'
  292. });
  293. }
  294. });
  295. }
  296. },
  297. fail: (err) => {
  298. console.error('获取用户信息失败:', err);
  299. uni.navigateTo({
  300. url: '/pages/Personal/Personal',
  301. fail: (err) => {
  302. console.error('页面跳转失败:', err);
  303. uni.showToast({
  304. title: '页面跳转失败',
  305. icon: 'none'
  306. });
  307. }
  308. });
  309. }
  310. });
  311. },
  312. // 获取当前职位配置
  313. getConfig(selectedJobId){
  314. uni.request({
  315. url: `${apiBaseUrl}/api/job/config/position/${this.jobId}`,
  316. method: 'GET',
  317. data: {
  318. openid: JSON.parse(uni.getStorageSync('userInfo')).openid
  319. },
  320. header: {
  321. 'content-type': 'application/x-www-form-urlencoded'
  322. },
  323. success: (res) => {
  324. // 身份验证成功后,继续提交用户信息
  325. console.log(res);
  326. if (res.statusCode === 200) {
  327. if (res.data.code === 2000) {
  328. uni.setStorageSync('configData', JSON.stringify(res.data.data))
  329. // 检查是否启用了视频宣讲功能
  330. const configData = res.data.data;
  331. const enableVideoPresentation = configData?.enable_video_presentation || false;
  332. if (enableVideoPresentation) {
  333. // 如果启用了视频宣讲,跳转到video-briefing页面
  334. const videoUrl = configData?.video_presentation_url || '';
  335. const nextPage = '/pages/Personal/Personal'; // 视频播放完成后的跳转页面
  336. uni.navigateTo({
  337. url: `/pages/video-briefing/video-briefing?src=${encodeURIComponent(videoUrl)}&next=${encodeURIComponent(nextPage)}`,
  338. fail: (err) => {
  339. console.error('跳转到视频宣讲页面失败:', err);
  340. // 如果跳转失败,执行原有的逻辑
  341. this.handleUserInfoAndNavigation();
  342. }
  343. });
  344. } else {
  345. // 如果未启用视频宣讲,执行原有的逻辑
  346. this.handleUserInfoAndNavigation();
  347. }
  348. } else {
  349. // 其他逻辑
  350. }
  351. } else {
  352. uni.hideLoading();
  353. }
  354. },
  355. fail: (err) => {
  356. uni.hideLoading();
  357. uni.showToast({
  358. title: '网络错误,请稍后重试',
  359. icon: 'none'
  360. });
  361. }
  362. });
  363. },
  364. async startInterview() {
  365. if (!this.checkLogin()) {
  366. return;
  367. }
  368. try {
  369. // 保存所选职位信息
  370. uni.setStorageSync('selectedJob', JSON.stringify(this.jobDetail));
  371. const userInfo = JSON.parse(uni.getStorageSync('userInfo'));
  372. const response = await applyJob({
  373. job_id: this.jobId,
  374. tenant_id:this.tenant_id||JSON.parse(uni.getStorageSync('userInfo')).tenant_id ||1,
  375. openid: userInfo.openid
  376. });
  377. if (response && response.id) {
  378. // 保存应用ID到本地
  379. uni.setStorageSync('appId', response.id);
  380. // 更新userInfo对象
  381. try {
  382. userInfo.appId = response.id;
  383. uni.setStorageSync('userInfo', JSON.stringify(userInfo));
  384. } catch (e) {
  385. console.error('更新用户信息失败:', e);
  386. }
  387. this.getConfig(userInfo.appId)
  388. // 导航到摄像头页面
  389. // uni.navigateTo({
  390. // url: '/pages/Personal/Personal',
  391. // fail: (err) => {
  392. // console.error('页面跳转失败:', err);
  393. // uni.showToast({
  394. // title: '页面跳转失败',
  395. // icon: 'none'
  396. // });
  397. // }
  398. // });
  399. }
  400. } catch (err) {
  401. console.error('申请职位失败:', err);
  402. uni.showToast({
  403. title: '申请职位失败,请重试',
  404. icon: 'none'
  405. });
  406. }
  407. },
  408. hasHtmlTags(text) {
  409. // 检查文本是否包含HTML标签
  410. const htmlRegex = /<[^>]*>/;
  411. return htmlRegex.test(text);
  412. },
  413. formatLocation(location) {
  414. if (!location) return '';
  415. // 处理字符串形式的数组
  416. if (typeof location === 'string' && location.startsWith('[')) {
  417. try {
  418. const locationArray = JSON.parse(location.replace(/'/g, '"'));
  419. return locationArray.join(' ') +''+ this.jobDetail.detailed_address ||JSON.parse(uni.getStorageSync('currentJobDetail')).detailed_address;
  420. } catch (e) {
  421. console.error('解析location失败:', e);
  422. return location;
  423. }
  424. }
  425. // 处理数组格式
  426. if (Array.isArray(location)) {
  427. return location.join(' ') +''+ this.jobDetail.detailed_address ||JSON.parse(uni.getStorageSync('currentJobDetail')).detailed_address;
  428. }
  429. // 处理对象格式
  430. if (typeof location === 'object' && location !== null) {
  431. const { province, city, district } = location;
  432. if (province && city) {
  433. return province + ' ' + city + (district ? ' ' + district : '');
  434. }
  435. }
  436. // 处理普通字符串格式
  437. return location +''+ this.jobDetail.detailed_address ||JSON.parse(uni.getStorageSync('currentJobDetail')).detailed_address;
  438. },
  439. // 更新地图位置信息
  440. async updateMapLocation(location,address) {
  441. try {
  442. // 如果location是字符串数组,转换为地址字符串
  443. let addressStr = '';
  444. if (typeof location === 'string' && location.startsWith('[')) {
  445. try {
  446. const locationArray = JSON.parse(location.replace(/'/g, '"'));
  447. addressStr = locationArray.join('') + address;
  448. } catch (e) {
  449. addressStr = location + address;
  450. }
  451. } else if (Array.isArray(location)) {
  452. addressStr = location.join('') + address;
  453. } else if (typeof location === 'object' && location !== null) {
  454. const { province, city, district } = location;
  455. addressStr = `${province || ''}${city || ''}${district || ''}${address}`;
  456. } else {
  457. addressStr = location + address;
  458. }
  459. console.log(addressStr,address);
  460. // 使用微信小程序的地址解析接口WJLBZ-SMQYZ-3RNX5-7J4LI-XTZD6-7IBZR
  461. uni.request({
  462. url: `https://apis.map.qq.com/ws/geocoder/v1/?address=${encodeURIComponent(addressStr)}&key=ZS4BZ-NKAA7-4VLXR-PHVI4-HAGPH-Z4FJ3`, // 需要替换为实际的地图Key
  463. success: (res) => {
  464. if (res.data.status === 0) {
  465. const { lat, lng } = res.data.result.location;
  466. this.mapInfo.latitude = lat;
  467. this.mapInfo.longitude = lng;
  468. this.mapInfo.markers = [{
  469. id: 1,
  470. latitude: lat,
  471. longitude: lng,
  472. title: addressStr
  473. }];
  474. } else {
  475. console.error('地址解析失败:', res);
  476. }
  477. },
  478. fail: (err) => {
  479. console.error('地址解析请求失败:', err);
  480. }
  481. });
  482. } catch (error) {
  483. console.error('更新地图位置失败:', error);
  484. }
  485. },
  486. openLocation() {
  487. if (this.mapInfo.latitude && this.mapInfo.longitude) {
  488. uni.openLocation({
  489. latitude: this.mapInfo.latitude,
  490. longitude: this.mapInfo.longitude,
  491. name: this.jobDetail.title,
  492. address: this.formatLocation(this.jobDetail.location),
  493. success: function () {
  494. console.log('导航打开成功');
  495. },
  496. fail: function (err) {
  497. console.error('导航打开失败:', err);
  498. uni.showToast({
  499. title: '导航打开失败',
  500. icon: 'none'
  501. });
  502. }
  503. });
  504. } else {
  505. uni.showToast({
  506. title: '暂无位置信息',
  507. icon: 'none'
  508. });
  509. }
  510. }
  511. }
  512. }
  513. </script>
  514. <style lang="scss" scoped>
  515. .job-detail-container {
  516. padding: 20rpx;
  517. position: relative;
  518. background-color: #f5f5f5;
  519. min-height: 100vh;
  520. }
  521. .job-header {
  522. padding: 20rpx 0;
  523. }
  524. .job-title {
  525. font-size: 36rpx;
  526. font-weight: bold;
  527. color: #333;
  528. margin-bottom: 10rpx;
  529. }
  530. .job-salary {
  531. font-size: 32rpx;
  532. color: #ff6b00;
  533. margin-bottom: 10rpx;
  534. }
  535. .job-department {
  536. font-size: 28rpx;
  537. color: #666;
  538. margin-bottom: 20rpx;
  539. }
  540. .job-requirements {
  541. display: flex;
  542. flex-direction: column;
  543. align-items: flex-start;
  544. flex-wrap: wrap;
  545. margin-bottom: 20rpx;
  546. }
  547. .requirement-item {
  548. display: flex;
  549. align-items: center;
  550. font-size: 26rpx;
  551. color: #666;
  552. }
  553. .dot {
  554. width: 13px;
  555. height: 13px;
  556. border-radius: 50%;
  557. background-color: #0052d9;
  558. margin-right: 10rpx;
  559. }
  560. .time-icon {
  561. width: 26rpx;
  562. height: 26rpx;
  563. background-color: #999;
  564. border-radius: 50%;
  565. margin-right: 10rpx;
  566. display: flex;
  567. justify-content: center;
  568. align-items: center;
  569. font-size: 18rpx;
  570. color: #fff;
  571. }
  572. .section {
  573. margin-bottom: 30rpx;
  574. background-color: #fff;
  575. border-radius: 16rpx;
  576. padding: 20rpx;
  577. }
  578. .section-title {
  579. font-size: 32rpx;
  580. font-weight: bold;
  581. color: #333;
  582. margin-bottom: 20rpx;
  583. }
  584. .map-container {
  585. position: relative;
  586. border-radius: 16rpx;
  587. overflow: hidden;
  588. }
  589. .map {
  590. width: 100%;
  591. height: 500rpx;
  592. border-radius: 16rpx;
  593. }
  594. .location-text {
  595. position: absolute;
  596. left: 20rpx;
  597. bottom: 20rpx;
  598. background-color: rgba(0, 0, 0, 0.6);
  599. color: #fff;
  600. font-size: 24rpx;
  601. padding: 6rpx 12rpx;
  602. border-radius: 6rpx;
  603. }
  604. .benefits-list {
  605. display: flex;
  606. flex-wrap: wrap;
  607. }
  608. .benefit-tag {
  609. background-color: #f5f5f5;
  610. color: #666;
  611. font-size: 26rpx;
  612. padding: 10rpx 20rpx;
  613. border-radius: 6rpx;
  614. margin-right: 20rpx;
  615. margin-bottom: 20rpx;
  616. }
  617. .description-subtitle {
  618. font-size: 28rpx;
  619. font-weight: bold;
  620. color: #333;
  621. margin-bottom: 20rpx;
  622. }
  623. .description-content {
  624. padding-left: 10rpx;
  625. }
  626. .description-item {
  627. display: flex;
  628. margin-bottom: 20rpx;
  629. /* * {
  630. background-color: #fff !important;
  631. color: #333 !important;
  632. }
  633. strong {
  634. font-weight: bold;
  635. } */
  636. }
  637. .blue-dot {
  638. min-width: 16rpx;
  639. height: 16rpx;
  640. border-radius: 50%;
  641. background-color: #0039b3 !important;
  642. margin-right: 10rpx;
  643. margin-top: 12rpx;
  644. }
  645. .description-item text {
  646. font-size: 26rpx;
  647. color: #666;
  648. line-height: 1.6;
  649. }
  650. .interview-button {
  651. position: fixed;
  652. right: 0;
  653. top: 50%;
  654. transform: translateY(-50%);
  655. background-color: #0039b3;
  656. color: #fff;
  657. writing-mode: vertical-lr;
  658. padding: 30rpx 20rpx;
  659. font-size: 32rpx;
  660. border-top-left-radius: 16rpx;
  661. border-bottom-left-radius: 16rpx;
  662. }
  663. .description-text {
  664. font-size: 26rpx;
  665. color: #333;
  666. line-height: 1.6;
  667. flex: 1;
  668. :deep(p) {
  669. margin: 0;
  670. }
  671. :deep(a) {
  672. color: #0039b3;
  673. text-decoration: none;
  674. }
  675. :deep(strong) {
  676. font-weight: bold;
  677. }
  678. }
  679. </style>