Xiao_123 2 kuukautta sitten
vanhempi
commit
9b0adafdc2
100 muutettua tiedostoa jossa 14986 lisäystä ja 509 poistoa
  1. 43 0
      App.vue
  2. 261 1
      api/common.js
  3. 13 0
      api/content.js
  4. 14 0
      api/enterprise.js
  5. 13 2
      api/file.js
  6. 38 0
      api/integral.js
  7. 79 0
      api/jobFair.js
  8. 26 0
      api/personalCenter.js
  9. 117 1
      api/position.js
  10. 330 0
      api/resume.js
  11. 187 0
      api/sign.js
  12. 128 0
      api/student.js
  13. 122 1
      api/user.js
  14. 50 0
      api/vip.js
  15. 80 0
      components/Advertisement/index.vue
  16. 104 48
      components/FilterList/index.vue
  17. 349 0
      components/FilterList/mFilter.vue
  18. 101 0
      components/FilterList/select.vue
  19. 49 0
      components/Navbar/index.vue
  20. 142 76
      components/PositionList/index.vue
  21. 94 0
      components/ResumeStatus/index.vue
  22. 15 11
      components/SwiperAd/index.vue
  23. 345 0
      components/searchCombox/index.vue
  24. 79 0
      components/studentDeliveryForm/index.vue
  25. 586 0
      components/ui/ct-popup/index.vue
  26. 45 0
      components/ui/ct-popup/keypress.js
  27. 51 4
      hooks/useDictionaries.js
  28. 432 0
      hooks/useIM.js
  29. 77 0
      hooks/useModal.js
  30. 35 0
      layout/components/auth-modal.vue
  31. 338 0
      layout/components/authModal/login/index.vue
  32. 248 0
      layout/components/authModal/necessaryInfo/index.vue
  33. 59 0
      layout/components/authModal/selectUserType/index.vue
  34. 306 0
      layout/components/authModal/selectUserType/studentInfoForm.vue
  35. 64 0
      layout/index.vue
  36. 0 1
      main.js
  37. 144 143
      package-lock.json
  38. 4 1
      package.json
  39. 310 7
      pages.json
  40. 27 0
      pages/addWebView/index.vue
  41. 218 4
      pages/index/communicate.vue
  42. 305 0
      pages/index/crowdsourcing.vue
  43. 305 58
      pages/index/my.vue
  44. 4 0
      pages/index/position.vue
  45. 483 0
      pages/index/welfare.vue
  46. 192 49
      pages/login/index.vue
  47. 146 0
      pagesA/balance/index.vue
  48. 1012 0
      pagesA/chart/index.vue
  49. 8 16
      pagesA/collect/company.vue
  50. 7 7
      pagesA/collect/index.vue
  51. 1 1
      pagesA/collect/position.vue
  52. 198 0
      pagesA/coupon/index.vue
  53. 34 32
      pagesA/info/index.vue
  54. 175 0
      pagesA/integral/index.vue
  55. 11 3
      pagesA/interview/index.vue
  56. 17 12
      pagesA/interview/item.vue
  57. 123 0
      pagesA/recommendation/index.vue
  58. 70 0
      pagesA/recommendation/list.vue
  59. 47 10
      pagesA/resume/index.vue
  60. 93 0
      pagesA/resumeAnalysis/components/advantage.vue
  61. 119 0
      pagesA/resumeAnalysis/components/avatarEdit.vue
  62. 180 0
      pagesA/resumeAnalysis/components/baseInfoEdit.vue
  63. 177 0
      pagesA/resumeAnalysis/components/educationExp.vue
  64. 137 0
      pagesA/resumeAnalysis/components/trainingExperience.vue
  65. 182 0
      pagesA/resumeAnalysis/components/workExperience.vue
  66. 221 0
      pagesA/resumeAnalysis/index.vue
  67. 9 0
      pagesA/resumeAnalysis/testData.js
  68. 50 0
      pagesA/resumeOnline/advantage.vue
  69. 214 0
      pagesA/resumeOnline/baseInfoEdit.vue
  70. 94 0
      pagesA/resumeOnline/dict.js
  71. 177 0
      pagesA/resumeOnline/educationExp.vue
  72. 637 0
      pagesA/resumeOnline/index.vue
  73. 177 0
      pagesA/resumeOnline/jobIntention.vue
  74. 131 0
      pagesA/resumeOnline/portrait.vue
  75. 134 0
      pagesA/resumeOnline/trainingExperience.vue
  76. 158 0
      pagesA/resumeOnline/vocationalSkills.vue
  77. 181 0
      pagesA/resumeOnline/workExperience.vue
  78. 53 21
      pagesA/seenMe/index.vue
  79. 174 0
      pagesA/student/addReport.vue
  80. 116 0
      pagesA/student/certificateDetail-copy.vue
  81. 174 0
      pagesA/student/certificateDetail.vue
  82. 116 0
      pagesA/student/certificateDetailCanvas.vue
  83. 73 0
      pagesA/student/enterpriseRecommendationLetter.vue
  84. 45 0
      pagesA/student/index.vue
  85. 207 0
      pagesA/student/information.vue
  86. 31 0
      pagesA/student/internshipButler.vue
  87. 72 0
      pagesA/student/internshipCertificate.vue
  88. 191 0
      pagesA/student/internshipRecord.vue
  89. 164 0
      pagesA/student/internshipReport.vue
  90. 196 0
      pagesA/vip/blockEnt/index.vue
  91. 191 0
      pagesA/vip/index.vue
  92. 41 0
      pagesA/vip/template/index.vue
  93. 698 0
      pagesA/vipPackage/index.vue
  94. 174 0
      pagesB/about/index.vue
  95. 36 0
      pagesB/agreement/CopyrightPolicy.vue
  96. 102 0
      pagesB/agreement/UserBehaviorNorms.vue
  97. 27 0
      pagesB/agreement/WorkplaceCommunityPolicy.vue
  98. 44 0
      pagesB/agreement/index.vue
  99. 51 0
      pagesB/agreement/privacy.vue
  100. 78 0
      pagesB/agreement/user.vue

+ 43 - 0
App.vue

@@ -1,7 +1,50 @@
 <script>
+// import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
 	export default {
 		onLaunch: function() {
+			// wx.showShareMenu({
+			// 	withShareTicket: true,
+			// 	menus: ['shareAppMessage', 'shareTimeline']
+			// })
+			// onShareAppMessage(() => {
+			// 	return {
+			// 		title: '门墩儿 专注顶尖招聘',
+			// 		path: '/pages/index/position',
+			// 		imageUrl: '../../static/img/share-poster.jpg'
+			// 	}
+			// })
+			// onShareTimeline(() => {
+			// 	return {
+			// 		title: '门墩儿 专注顶尖招聘',
+			// 		path: '/pages/index/position',
+			// 		imageUrl: '../../static/img/share-poster.jpg'
+			// 	}
+			// })
 			console.log('App Launch')
+			uni.setStorageSync('firstOpen', true)
+
+			const updateManager = uni.getUpdateManager();
+			updateManager.onCheckForUpdate(function(res){});
+			
+			updateManager.onUpdateReady(function(res){
+				uni.showModal({
+					title: '更新提示',
+					content:'发现新版本,是否重启应用',
+					success(res) {
+						if(res.confirm) {
+							updateManager.applyUpdate();
+						}
+					}
+				})
+			});
+			
+			updateManager.onUpdateFailed(function(res) {
+				//新本版下载失败
+				uni.showModal({
+					title:'失败提示',
+					content:'新版本下载失败,请手动删掉小程序后重新搜索进入小程序'
+				})
+			})
 		},
 		onShow: function() {
 			console.log('App Show')

+ 261 - 1
api/common.js

@@ -41,6 +41,31 @@ export const smsLogin = (data) => {
   })
 }
 
+// 微信小程序的一键登录
+export const weChatLogin = (data) => {
+  return request({
+    url: '/app-api/menduner/system/auth/weixin-mini-app-login',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false
+    }
+  })
+}
+
+// 短信登录
+export const userRegister = (data) => {
+  return request({
+    url: '/app-api/menduner/system/auth/register',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false
+    }
+  })
+}
+
+
 // 密码登录
 export const passwordLogin = (data) => {
   return request({
@@ -53,6 +78,18 @@ export const passwordLogin = (data) => {
   })
 }
 
+// 二维码扫码用户注册并登录
+export const shareUserRegister = (data) => {
+  return request({
+    url: '/app-api/menduner/system/auth/register',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false
+    }
+  })
+}
+
 // 退出登录
 export const logout = () => {
   return request({
@@ -178,4 +215,227 @@ export const getAreaTreeData = () => {
       auth: false
     }
   })
-}
+}
+
+
+
+
+// 同步最近会话
+export const getConversationSync = async (data) => {
+  return request({
+    url: '/app-api/im/conversation/sync',
+    method: 'POST',
+    data,
+    custom: {
+      openEncryption: true,
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+
+// 获取聊天秘钥信息
+export const getChatKey = async (data) => {
+  return request({
+    url: '/app-api/im/user/get',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 同步最近会话
+export const getMessageSync = async (data) => {
+  return request({
+    url: '/app-api/im/im/channel/messagesync',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+// 设置最近会话未读数量
+export const setUnread = async (data) => {
+  return request({
+    url: '/app-api/im/conversations/setUnread',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+// 设置最近会话未读数量
+export const deleteConversation = async (data) => {
+  return request({
+    url: '/app-api/im/conversation/delete',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+// 获取type类型聊天记录
+export const getMessageType = async (data) => {
+  return request({
+    url: '/app-api/im/im/history/messages',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 求职端-根据邀请人id获取面试邀约列表
+export const getInterviewInviteListByInviteUserId = async (inviteUserId) => {
+  return request({
+    url: `/app-api/menduner/system/interview-invite/get/list/by/${inviteUserId}`,
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 内容管理-广告
+export const getWebContent = async () => {
+  return request({
+    url: `/app-api/menduner/system/web-content/get?id=1`,
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}
+
+// 联系我们
+// 提交猎寻服务
+export const huntSubmit = async (data) => {
+  return request({
+    url: '/admin-api/menduner/system/hunt/submit',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}
+
+// 获得微信小程订阅模板列表
+// export const getSubscribeTemplateList = async () => {
+//   return request({
+//     url: `/app-api/menduner/system/social-user/get-subscribe-template-list`,
+//     method: 'GET',
+//     custom: {
+//       showLoading: false,
+//       auth: false
+//     }
+//   })
+// }
+
+// 求职端交易订单  创建
+export const orderCreated = async (data) => {
+  return request({
+    // url: '/app-api/menduner/system/trade/order/create',
+    url: '/app-api/menduner/system/trade/order/wx-program/create',
+    method: 'POST',
+    data,
+    custom: {
+      openEncryption: true,
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 求职端交易订单  创建
+export const getOrder = async (params) => {
+  return request({
+    url: '/app-api/menduner/system/trade/order/get/unpaid',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 获得社交用户
+export const getSocialUser = async (type) => {
+  return request({
+    url: `/app-api/menduner/system/social-user/get?type=${type}`,
+    method: 'GET',
+    // params,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+//  获得支付订单 (轮巡支付状态)
+export const getOrderPayStatus = async (params) => {
+  return request({
+    url: '/app-api/pay/order/get',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 获得指定应用的开启的支付渠道编码列表
+export const getEnableCodeList = async (params) => {
+  return request({
+    url: '/app-api/pay/channel/get-enable-code-list',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 提交支付订单
+export const payOrderSubmit = async (data) => {
+  return request({
+    url: '/app-api/pay/order/submit',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 社交绑定,使用 code 授权码
+export const socialUserBind = async (data) => {
+  return request({
+    url: '/app-api/menduner/system/social-user/bind',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}

+ 13 - 0
api/content.js

@@ -0,0 +1,13 @@
+import request from "@/utils/request"
+
+// 获取最新的早报资讯
+export const getMorningNewsArticle = () => {
+  return request({
+    url: '/app-api/menduner/system/morning-news/get',
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}

+ 14 - 0
api/enterprise.js

@@ -0,0 +1,14 @@
+import request from "@/utils/request"
+
+// 获取精选企业列表
+export const getHotEnterprise = (params) => {
+  return request({
+    url: '/app-api/menduner/system/job/advertised/get/hot/enterprise',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}

+ 13 - 2
api/file.js

@@ -1,18 +1,29 @@
 import { baseUrl, tenantId, apiPath } from '@/utils/config'
 
+// const uploadApi = {
+//   file: '/menduner/system/file/upload',
+//   img: '/infra/file/upload'
+// }
+
 // 文件上传
-export const uploadFile = (file) => {
+export const uploadFile = (file, path) => {
   uni.showLoading({
     title: '上传中'
   })
   return new Promise((resolve, reject) => {
     uni.uploadFile({
-      url: baseUrl + apiPath + '/infra/file/upload',
+      url: baseUrl + apiPath + '/menduner/system/file/upload',
       filePath: file,
+      formData: {
+        path
+      },
       name: 'file',
       header: {
         Accept: '*/*',
         'tenant-id': tenantId,
+        'Authorization': 'Bearer ' + uni.getStorageSync('token'),
+        'Accept-Language': 'zh_CN',
+        'terminal': 'mp-weixin'
       },
       success: (uploadFileRes) => {
         let result = JSON.parse(uploadFileRes.data)

+ 38 - 0
api/integral.js

@@ -0,0 +1,38 @@
+import request from "@/utils/request"
+
+// 获取规则配置跟踪列表
+export const getRewardEventTrackList = () => {
+  return request({
+    url: '/admin-api/menduner/reward/event-track/list',
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}
+
+// 规则接口触发
+export const rewardEventTrackClick = (url) => {
+  return request({
+    url: '/admin-api/menduner/reward/event-track/click?url=' + url,
+    method: 'POST',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 推荐任务
+export const getTaskList = (params) => {
+  return request({
+    url: '/admin-api/menduner/reward/event-track/get/mark/task',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}

+ 79 - 0
api/jobFair.js

@@ -0,0 +1,79 @@
+import request from "@/utils/request"
+
+// 获得招聘会列表
+export const getJobFairList = (params) => {
+  return request({
+    url: '/app-api/menduner/system/job-fair/list',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}
+
+// 获得招聘会-招聘会详情
+export const getJobFair = (id) => {
+  return request({
+    url: '/app-api/menduner/system/job-fair/get',
+    method: 'GET',
+    params: { id },
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}
+
+// 招聘会企业分页查询
+export const getJobFairEnterprisePage = (params) => {
+  return request({
+    url: '/app-api/menduner/system/job-fair/enterprise/page',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}
+
+// 根据企业id查询招聘会职位列表
+export const getJobFairEntJobPage = (params) => {
+  return request({
+    url: '/app-api/menduner/system/job-fair/detail/page',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}
+
+// 保存分享参数
+export const saveShareQuery = (data) => {
+  return request({
+    url: '/app-api/menduner/system/share/share',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}
+
+// 根据id获取分享参数
+export const getShareQueryById = (params) => {
+  return request({
+    url: '/app-api/menduner/system/share/get',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}

+ 26 - 0
api/personalCenter.js

@@ -0,0 +1,26 @@
+
+import request from "@/utils/request"
+// 同意邀约面试
+export const userInterviewInviteConsent = async (params) => {
+  return request({
+    url: '/app-api/menduner/system/interview-invite/consent',
+    method: 'POST',
+    params,
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}
+
+// 拒绝邀约面试
+export const userInterviewInviteReject = async (id) => {
+  return request({
+    url: `/app-api/menduner/system/interview-invite/reject?id=${id}`,
+    method: 'POST',
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}

+ 117 - 1
api/position.js

@@ -13,6 +13,19 @@ export const getJobAdvertisedSearch = (params) => {
   })
 }
 
+// 推荐职位列表
+export const getPromotedPosition = (params) => {
+  return request({
+    url: '/app-api/menduner/system/job/advertised/get/recommended',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}
+
 // 职位详情
 export const getPositionDetails = (params) => {
   return request({
@@ -26,7 +39,6 @@ export const getPositionDetails = (params) => {
   })
 }
 
-
 // 效验招聘职位是否投递
 export const jobCvRelCheckSend = (params) => {
   return request({
@@ -40,6 +52,19 @@ export const jobCvRelCheckSend = (params) => {
   })
 }
 
+// 效验“招聘会”职位是否投递
+export const jobFairCvRelCheckSend = (params) => {
+  return request({
+    url: '/app-api/menduner/system/job-cv-rel/job-fair/send/check',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
 // 效验求职者是否收藏该职位
 export const getJobFavoriteCheck = (params) => {
   return request({
@@ -93,3 +118,94 @@ export const jobCvRelSend = (data) => {
     }
   })
 }
+
+// 众聘分享-投递简历
+export const jobCvRelHireSend = (data) => {
+  return request({
+    url: '/app-api/menduner/system/job-cv-rel/hire/recommend/send',
+    method: 'POST',
+    data,
+    custom: {
+      auth: true,
+      showLoading: false
+    }
+  })
+}
+
+// 获取众聘职位分页
+export const getJobAdvertisedHire = (params) => {
+  return request({
+    url: '/app-api/menduner/system/job/advertised/get/hire',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}
+
+// 获取统计信息
+export const getRecommendCount = (params) => {
+  return request({
+    url: '/app-api/menduner/system/job-cv-rel/hire/get/recommend/count',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 二维码拉新记录
+export const getInviteRecord = (params) => {
+  return request({
+    url: '/app-api/menduner/system/person/get/invite/person/page',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 获取统计信息
+export const getRecommendationList = (params) => {
+  return request({
+    url: '/app-api/menduner/system/job-cv-rel/hire/page',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 根据id查询分享的职位id与推荐人id
+export const getShareDetail = (params) => {
+  return request({
+    url: '/app-api/menduner/system/job/advertised/get/share',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}
+
+// 职位详情-获取相似职位
+export const getSimilarPosition = (params) => {
+  return request({
+    url: '/app-api/menduner/system/job/advertised/get/acquainted',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}

+ 330 - 0
api/resume.js

@@ -0,0 +1,330 @@
+import request from "@/utils/request"
+
+// 保存基本信息
+export const saveResumeBasicInfo = async (data) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/info/save',
+    method: 'POST',
+    data,
+    custom: {
+      openEncryption: true,
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// // 保存个人优势
+// export const saveResumeAdvantage = async (data) => {
+//   return await request.post({
+//     url: '/app-api/menduner/system/person/resume/advantage/save',
+//     data
+//   })
+// }
+
+// 保存培训经历
+export const saveResumeTrainExp = async (data) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/train/exp/save',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 删除培训经历
+export const deleteResumeTrainExp = async (id) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/train/exp/remove?id=' + id,
+    method: 'DELETE',
+    custom: {
+      auth: true,
+      showLoading: false
+    }
+  })
+}
+
+// 获取培训经历
+export const getResumeTrainExp = async () => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/get/train/exp',
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// // 获取-教育经历
+export const getResumeEduExp = async () => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/get/edu/exp',
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 删除-教育经历
+export const deleteResumeEduExp = async (id) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/edu/exp/remove?id=' + id,
+    method: 'DELETE',
+    custom: {
+      auth: true,
+      showLoading: false
+    }
+  })
+}
+
+// 保存-教育经历
+export const saveResumeEduExp = async (data) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/edu/exp/save',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 获取-工作经历
+export const getResumeWorkExp = async () => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/get/work/exp',
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 删除-工作经历
+export const deleteResumeWorkExp = async (id) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/work/exp/remove?id=' + id,
+    method: 'DELETE',
+    custom: {
+      auth: true,
+      showLoading: false
+    }
+  })
+}
+
+// 保存-工作经历
+export const saveResumeWorkExp = async (data) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/work/exp/save',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// // 保存项目经历
+// export const saveResumeProjectExp = async (data) => {
+//   return await request.post({
+//     url: '/app-api/menduner/system/person/resume/project/exp/save',
+//     data
+//   })
+// }
+
+// // 删除项目经历
+// export const deleteResumeProjectExp = async (id) => {
+//   return await request.delete({
+//     url: '/app-api/menduner/system/person/resume/project/exp/remove?id=' + id
+//   })
+// }
+
+// 获取项目经历
+// export const getResumeProjectExp = async () => {
+//   return request({
+//     url: '/app-api/menduner/system/person/resume/get/project/exp',
+//     method: 'GET',
+//     custom: {
+//       showLoading: false,
+//       auth: true
+//     }
+//   })
+// }
+
+// 获取-技能树形
+export const getSkillTree = async () => {
+  return request({
+    url: '/app-api/menduner/system/skill/get/tree',
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 获取-职业技能
+export const getResumePersonSkill = async () => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/get/person/skill',
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 删除-职业技能
+export const deleteResumePersonSkill = async (id) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/person/skill/remove?id=' + id,
+    method: 'DELETE',
+    custom: {
+      auth: true,
+      showLoading: false
+    }
+  })
+}
+
+// 保存-职业技能
+export const saveResumePersonSkill = async (data) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/person/skill/save',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 保存求职意向
+export const saveResumeJobInterested = async (data) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/job/interested/save',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 删除求职意向
+export const deleteResumeJobInterested = async (id) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/job/interested/remove?id=' + id,
+    method: 'DELETE',
+    custom: {
+      auth: true,
+      showLoading: false
+    }
+  })
+}
+
+// // 获取求职意向
+export const getResumeJobInterested = async () => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/get/job/interested',
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// // 根据专业名称模糊搜索
+export const schoolMajorByName = async (params) => {
+  return request({
+    url: '/app-api/menduner/system/major/search/by/name',
+    params,
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 根据学校名称模糊搜索
+export const schoolSearchByName = async (params) => {
+  return request({
+    url: '/app-api/menduner/system/school/search/by/name',
+    params,
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 根据企业名称模糊搜索
+export const enterpriseSearchByName = async (params) => {
+  return request({
+    url: '/app-api/menduner/system/enterprise/search/by/name',
+    params,
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// // 保存附件
+// export const savePersonResumeCv = async (data) => {
+//   return await request.post({
+//     url: '/app-api/menduner/system/person/resume/person/cv/save',
+//     data
+//   })
+// }
+
+// // 删除附件
+// export const deletePersonResumeCv = async (id) => {
+//   return await request.delete({
+//     url: '/app-api/menduner/system/person/resume/person/cv/remove?id=' + id
+//   })
+// }
+
+// // 获取附件列表
+// export const getPersonResumeCv = async () => {
+//   return await request.get({
+//     url: '/app-api/menduner/system/person/resume/get/person/cv'
+//   })
+// }
+
+// // 修改求职类型
+// export const updateJobStatus = async (data) => {
+//   return await request.post({
+//     url: '/app-api/menduner/system/person/resume/job/status/update?status=' + data,
+//   })
+// }
+
+// // 修改人才头像
+// export const updatePersonAvatar = async (url) => {
+//   return await request.post({
+//     url: `/app-api/menduner/system/person/resume/avatar/update?avatar=${url}`
+//   })
+// }
+
+// // 修改个人画像
+// export const savePersonPortrait = async (data) => {
+//   return await request.post({
+//     url: '/app-api/menduner/system/person/resume/tag/update',
+//     data
+//   })
+// }

+ 187 - 0
api/sign.js

@@ -0,0 +1,187 @@
+import request from "@/utils/request"
+
+// 签到
+export const createRewardSignInRecord = (data) => {
+  return request({
+    url: '/app-api/menduner/reward/sign-in/record/create',
+    method: 'POST',
+    data,
+    custom: {
+      auth: true,
+      showLoading: false
+    }
+  })
+}
+
+
+// 获得个人签到统计
+export const getRewardSignInRecordSummary = () => {
+  return request({
+    url: '/app-api/menduner/reward/sign-in/record/get-summary',
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 获得签到记录分页
+export const getRewardSignInRecordPage = (pageNo, pageSize) => {
+  return request({
+    url: `/app-api/menduner/reward/sign-in/record/page?pageSize=${pageSize}&pageNo=${pageNo}`,
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 获取签到规则列表
+export const getRewardSignInConfigList = () => {
+  return request({
+    url: '/app-api/menduner/reward/sign-in/config/list',
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}
+
+// 获取账户余额
+export const getAccountBalance = () => {
+  return request({
+    url: '/app-api/pay/wallet/get',
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 获取积分余额
+export const getUserAccount = () => {
+  return request({
+    url: '/app-api/menduner/system/mde-user/get/account',
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 福利 领领取优惠券
+export const takeCoupon = (templateId) => {
+  return request({
+    url: '/app-api/promotion/coupon/take',
+    method: 'POST',
+    data: { templateId },
+    custom: {
+      auth: true,
+      showLoading: true,
+      loadingMsg: '领取中',
+      showSuccess: true,
+      successMsg: '领取成功'
+    }
+  })
+}
+// 福利 优惠券ID
+export const getDiyTemplate = (ids) => {
+  return request({
+    url: '/app-api/promotion/coupon-template/list-by-ids',
+    method: 'GET',
+    params: {
+      ids
+    },
+    custom: {
+      auth: true,
+      showError: false,
+      showLoading: false,
+    }
+  })
+}
+// 福利 获取商城模板
+export const getDiyTemplateUsed = () => {
+  return request({
+    url: '/app-api/promotion/diy-template/used',
+    method: 'GET',
+    custom: {
+      auth: true,
+      showError: false,
+      showLoading: false,
+    }
+  })
+}
+
+
+
+// 获取优惠券模板分页
+export const getCouponTemplatePage = (params) => {
+  return request({
+    url: '/app-api/promotion/coupon-template/page',
+    method: 'GET',
+    params,
+    custom: {
+      auth: false,
+      showError: false,
+      showLoading: false,
+    }
+  })
+}
+
+// 我的优惠劵列表
+export const getCouponPage = (params) => {
+  return request({
+    url: '/app-api/promotion/coupon/page',
+    method: 'GET',
+    params,
+    custom: {
+      auth: true,
+      showError: false,
+      showLoading: false,
+    }
+  })
+}
+
+// 获得钱包充值记录分页
+export const getUserWalletRechargePage = async (params) => {
+  return request({
+    url: '/app-api/pay/wallet-recharge/page',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 获得钱包流水分页
+export const getUserWalletTransactionPage = async (params) => {
+  return request({
+    url: '/app-api/pay/wallet-transaction/page',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 账户变动记录
+export const getEnterpriseAccountRecordPage = async (params) => {
+  return request({
+    url: '/app-api/menduner/system/recruit/enterprise/account/record/page',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}

+ 128 - 0
api/student.js

@@ -0,0 +1,128 @@
+import request from "@/utils/request"
+
+// 获取企业推荐信列表
+export const getRecommendationPage = async (data) => {
+	return request({
+		url: '/app-api/flames/student/recommendation/list',
+		method: 'POST',
+		data,
+		custom: {
+			openEncryption: true,
+			showLoading: false,
+			auth: true
+		}
+	})
+}
+
+// 获取实习证书列表
+export const getEnterpriseCertificateList = async (data) => {
+	return request({
+		url: '/app-api/flames/student/internship/certificate/list',
+		method: 'POST',
+		data,
+		custom: {
+			openEncryption: true,
+			showLoading: false,
+			auth: true
+		}
+	})
+}
+
+// 获取学校详情
+export const getSchoolDetails = async (data) => {
+	return request({
+		url: '/app-api/flames/school/detail',
+		method: 'POST',
+		data,
+		custom: {
+			openEncryption: true,
+			showLoading: false,
+			auth: true
+		}
+	})
+}
+
+// 获得学生实习记录分页
+export const getStudentPage = async (params) => {
+	return request({
+		url: '/app-api/menduner/system/student/page',
+		method: 'GET',
+		params,
+		custom: {
+			auth: true
+		}
+	})
+}
+
+// 获得学生实习报告列表
+export const getStudentReportList = async (params) => {
+	return request({
+		url: '/app-api/menduner/system/student/get/report/list',
+		method: 'GET',
+		params,
+		custom: {
+			auth: true
+		}
+	})
+}
+
+// 保存学生实习报告
+export const saveStudentReport = async (data) => {
+	return request({
+		url: '/app-api/menduner/system/student/report/save',
+		method: 'POST',
+		data,
+		custom: {
+			openEncryption: true,
+			showLoading: false,
+			auth: true
+		}
+	})
+}
+
+// 获取学生实习的企业列表
+export const getStudentPracticeCompanyList = async () => {
+	return request({
+		url: '/app-api/menduner/system/student/record-enterprise/list',
+		method: 'GET',
+		custom: {
+			auth: true
+		}
+	})
+}
+
+// 获取学校信息列表
+export const getSchoolList = async (params) => {
+	return request({
+		url: '/app-api/menduner/system/school/info/list',
+		params,
+		method: 'GET',
+		custom: {
+			auth: false
+		}
+	})
+}
+
+// 根据学校id获取院系列表
+export const getDepartmentListBySchoolId = async (params) => {
+	return request({
+		url: '/app-api/menduner/system/school/organization/list',
+		params,
+		method: 'GET',
+		custom: {
+			auth: false
+		}
+	})
+}
+
+// 获取专业列表
+export const getMajorList = async (params) => {
+	return request({
+    url: '/app-api/menduner/system/major/list',
+		params,
+		method: 'GET',
+		custom: {
+			auth: false
+		}
+	})
+}

+ 122 - 1
api/user.js

@@ -7,6 +7,7 @@ export const getBaseInfo = (params) => {
     method: 'GET',
     params,
     custom: {
+      openEncryption: true,
       showLoading: false,
       auth: true
     }
@@ -20,6 +21,7 @@ export const getUserInfo = (params) => {
     method: 'GET',
     params,
     custom: {
+      openEncryption: true,
       showLoading: false,
       auth: true
     }
@@ -38,6 +40,33 @@ export const getPersonResumeCv = () => {
   })
 }
 
+// 附件简历解析2
+export const resumeParser2 = (params) => {
+  return request({
+    url: '/admin-api/menduner/system/online/resume/parser2',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 简历解析-保存简历信息
+export const saveResumeInfo = (data) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/save',
+    method: 'POST',
+    data,
+    custom: {
+      openEncryption: true,
+      auth: true,
+      showLoading: false
+    }
+  })
+}
+
 // 获取收藏的招聘职位列表
 export const getJobFavoriteList = (params) => {
   return request({
@@ -261,6 +290,46 @@ export const saveBaseInfo = (data) => {
     url: '/app-api/menduner/system/person/resume/info/save',
     method: 'POST',
     data,
+    custom: {
+      openEncryption: true,
+      auth: true,
+      showLoading: false
+    }
+  })
+}
+
+// 根据类型获取标签信息
+export const getTagTreeDataApi = async (params) => {
+  return request({
+    url: '/admin-api/menduner/system/tag/get/by/type',
+    method: 'GET',
+    params,
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}
+
+// 修改个人画像
+export const savePersonPortrait = (data) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/tag/update',
+    method: 'POST',
+    data,
+    custom: {
+      auth: true,
+      showLoading: false
+    }
+  })
+}
+
+// 保存个人优势
+export const saveResumeAdvantage = (data) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/advantage/save',
+    method: 'POST',
+    data,
     custom: {
       auth: true,
       showLoading: false
@@ -281,4 +350,56 @@ export const updatePersonAvatar = (avatar) => {
       showLoading: false
     }
   })
-}
+}
+
+// 获取职位分享小程序二维码
+export const getJobAdvertisedShareQrcode = (data, custom) => {
+  return request({
+    url: '/app-api/menduner/system/social-user/wxa-qrcode',
+    method: 'POST',
+    data,
+    custom: {
+      auth: custom?.noAuth ? false : true,
+      showLoading: false
+    }
+  })
+}
+
+// 保存简易基本信息
+export const savePersonSimpleInfo = (data) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/info/simple/save',
+    method: 'POST',
+    data,
+    custom: {
+      auth: true,
+      showLoading: false
+    }
+  })
+}
+
+// 保存学生基本信息
+export const saveStudentSimpleInfo = (data) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/student/save',
+    method: 'POST',
+    data,
+    custom: {
+      auth: true,
+      showLoading: false
+    }
+  })
+}
+
+// 获取学生基本信息
+export const getStudentInfo = (data) => {
+  return request({
+    url: '/app-api/menduner/system/person/resume/student/get',
+    method: 'POST',
+    data,
+    custom: {
+      auth: true,
+      showLoading: false
+    }
+  })
+}

+ 50 - 0
api/vip.js

@@ -0,0 +1,50 @@
+import request from "@/utils/request"
+
+// 屏蔽企业-获得屏蔽的企业
+export const getBlockEnterpriseList = async () => {
+  return request({
+    url: '/app-api/menduner/system/person/enterprise-block/page',
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 屏蔽企业-加入屏蔽
+export const handleBlockEnterprise = async (data) => {
+  return request({
+    url: '/app-api/menduner/system/person/enterprise-block/block',
+    method: 'POST',
+    data,
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 屏蔽企业-取消屏蔽
+export const handleUnBlockEnterprise = async (enterpriseId) => {
+  return request({
+    url: '/app-api/menduner/system/person/enterprise-block/un-block?enterpriseId=' + enterpriseId,
+    method: 'DELETE',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
+}
+
+// 套餐列表
+export const getMembershipPackageList = async (enterpriseId) => {
+  return request({
+    url: 'app-api/menduner/system/user-package/list',
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: false
+    }
+  })
+}

+ 80 - 0
components/Advertisement/index.vue

@@ -0,0 +1,80 @@
+<template>
+  <view >
+		<uni-popup ref="inputDialog"  class="pop" :mask-click="false" @close="closeAdd">
+			<view class="popup-content" >
+					<swiper class="swiper-box" @change="swiperChange">
+						<swiper-item v-for="(item ,index) in swiperList" :key="index" class="f-straight">
+							<image class="img-item" :src="item.img" @click="skipLink(item)" mode="widthFix"></image>
+							<view @click="closeAdd" class="f-horizon-center">
+								<uni-icons style="display: flex;justify-content: flex-end;" type="closeempty"	size="40"	color="#fff"/>
+							</view>
+						</swiper-item>
+					</swiper>
+			</view>
+		</uni-popup>
+	</view>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getWebContent } from '@/api/common'
+
+const current = ref(0)
+const inputDialog = ref()
+const swiperList = ref([])
+
+// 获取广告图
+const getSystemWebContent = async () => {
+  const { data } = await getWebContent()
+  swiperList.value = data.appAdvertisement || []
+}
+getSystemWebContent()
+
+const closeAdd = () => {
+  inputDialog.value.close()
+	uni.removeStorageSync('firstOpen')
+}
+
+const openDialog = () => {
+  inputDialog.value.open()
+}
+
+const skipLink = ({ link, title }) => {
+	closeAdd()
+	if (link) {
+		const url = link.indexOf('http') !== -1 ? `/pages/addWebView/index?url=${link}&title=${title}` : link
+		uni.navigateTo({ url })
+	}
+}
+
+const swiperChange = (e) => {
+  current.value = e.detail.current
+}
+
+onMounted(() => {
+  if(uni.getStorageSync('firstOpen')) openDialog()
+})
+</script>
+
+<style scoped lang="scss">
+.popup-content {
+	align-items: center;
+	justify-content: center;
+	width: 80vw;
+	height: 100vh;
+}
+.img-item{
+	width: 100%;
+	height: 60vh;
+	margin: 0;
+	border-radius: 20px;
+}
+.swiper-box{
+	height: 100vh;
+}
+.f-straight{
+	display: flex;
+	justify-content: center;
+	flex-direction: column;
+}
+</style>

+ 104 - 48
components/FilterList/index.vue

@@ -1,30 +1,39 @@
 <template>
   <view class="labelColor itemBox" style="height: 45px;">
-    <view class="item" v-for="item in filterList" :key="item[props.idValue]">
-      <uni-data-picker
-        ref="picker"
-        v-slot:default="{ data, error }"
-        :localdata="item.array" 
-        :clear-icon="true"
-        :popup-title="'请选择' + item[labelValue]" 
-        :map="item.map || { text: 'label', value: 'value' }"
-         @change="e => handleClick(e, item)"
-         @nodeclick="handleChange"
+    <view class="item" v-for="(item) in filterList" :key="item[props.idValue]">
+      <m-filter
+        class="itemFilter"
+        :items="item.array"
+        :label="item.label"
+        :multiple="item.multiple"
+        :item-label="item.itemLabel"
+        :item-value="item.itemValue"
+        :popupStyle="{ 'padding-bottom': '80px' }"
+        @change="($event, $name) => handleClick($event, $name, item)"
       >
-        <view v-if="error" class="error">
-          <text>{{error}}</text>
-        </view>
-        <view v-else-if="data.length" class="selected">
-          <view class="selected-item d-flex align-center">
-            <view class="ellipsis" style="max-width: 100rpx;">{{ data.length > 1 ? data[data.length - 1].text : data[0].text }}</view>
-            <uni-icons type="closeempty" size="18" @click="data = [], handleClear"></uni-icons>
+        <view
+          :class="(item.multiple && item.value?.length) || (!item.multiple && item.value) ? 'active' : ''"
+          class="name"
+        >
+          <view class="over">
+            {{ item.name ?? item.label }}
           </view>
+          <template v-if="item.multiple && item.value?.length > 1">
+            <uni-icons
+              class="point"
+              type="smallcircle-filled"
+              color="#00B760"
+              size="6"
+            />
+            {{item.value.length}}
+          </template>
+          <uni-icons
+            type="down"
+            color=""
+            size="12"
+          />
         </view>
-        <view v-else>
-          {{ item[labelValue] }}
-          <uni-icons type="icon-arrow-sortdown-smal" custom-prefix="iconfont" color="#999"/>
-        </view>
-      </uni-data-picker>
+      </m-filter>
     </view>
   </view>
 </template>
@@ -32,35 +41,60 @@
 <script setup>
 import { ref, watch } from 'vue'
 import { getDict } from '@/hooks/useDictionaries'
+import MFilter from './mFilter.vue'
 
 const emit = defineEmits(['change'])
 const props = defineProps({
   list: { type: Array, default: () => [] },
   idValue: { type: String, default: 'id' },
   labelValue: { type: String, default: 'label' },
-  selectIdValue: { type: String, default: 'id' },
-  selectLabelValue: { type: String, default: 'label' },
-  useApiData: { type: Boolean, default: true },
+  // selectIdValue: { type: String, default: 'id' },
+  // selectLabelValue: { type: String, default: 'label' },
+  // useApiData: { type: Boolean, default: true },
   lazy: { type: Boolean, default: false },
 })
 
-const picker = ref()
-const handleClick = (e, item) => {
-  const obj = e.detail.value
-  item.value = obj.length && obj.length === 1 ? obj[0].value : obj[obj.length - 1].value
-  emit('change', item.key, item.value)
-}
+// const popup = ref()
 
-const handleClear = () => {
-  console.log(picker.value)
+// const handleOpen = (index) => {
+//   popup.value[index].open('bottom')
+// }
+// const handleChangeItem = (i, item) => {
+//   if (i === item.active) {
+//     return
+//   }
+//   item.active = i
+// }
+
+
+const handleClick = (e, name, item) => {
+  item.value = e
+  item.name = name ?? item.label
+  // const obj = e.detail.value?.length ? e.detail.value[e.detail.value.length-1] : {}
+  // const obj0 = e.detail.value?.length ? e.detail.value[0] : {}
+  // //
+  // let val = obj.value
+  // item.value = obj.value
+  // item.text = obj.text || ''
+  // if (typeof val === 'string' && val.includes('unlimited') && obj0) {
+  //   val = Number.isInteger(obj0.id) ? Number(val.split('unlimited')[0]) : val.split('unlimited')[0]
+  // }
+  emit('change', item.key, e)
 }
 
-const handleChange = () => {}
+// const handleClear = (item, index) => {
+//   item.value = e.multiple ? [] : null
+//   item.text = null
+//   emit('change', item.key, item.value)
+// }
 
 // 获取字典数据
 const getData = (e) => {
   getDict(e.dictType, e.map ? {} : null, e.map ? e.dictType : 'dict').then(({ data }) => {
     e.array = data.data
+    e.itemLabel = e.map?.text
+    e.itemValue = e.map?.value
+    e.value = e.multiple ? [] : null
   })
 }
 
@@ -70,11 +104,19 @@ const setItemSelectData = () => {
   })
 }
 
+// clone list
 const filterList = ref([])
 watch(() => props.list, 
   (newVal) => {
-    filterList.value = newVal ? [...newVal] : []
-    if (filterList.value.length && !props.lazy) setItemSelectData()
+    if (!newVal) {
+      filterList.value = []
+      return
+    }
+    filterList.value = newVal
+    
+    if (!props.lazy) {
+      setItemSelectData()
+    }
   },
   { immediate: true }
 )
@@ -83,24 +125,38 @@ watch(() => props.list,
 
 <style scoped lang="scss">
 .labelColor { color: #5c5c5c; }
-.marginR5 { margin-right: 5px; }
 .itemBox {
-  width: 100%;
   display: flex;
-  justify-content: center;
-  flex-wrap: wrap;
+  justify-content: space-between;
   .item {
-    width: 20%;
     display: flex;
     align-items: center;
+    justify-content: center;
+    flex: 1;
+    width: 0;
     font-size: 14px;
-    flex-wrap: wrap;
-    // margin: 0 3px;
+    .itemFilter {
+      width: 100%;
+    }
+    .name {
+      width: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      &.active {
+        color: #00B760;
+      }
+      .over {
+        overflow: hidden;
+        white-space: nowrap;
+      }
+      .point {
+        margin: 0 5px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+    }
   }
 }
-.popup {
-  height: 50vh;
-  background-color: #fff;
-  text-align: center;
-}
 </style>

+ 349 - 0
components/FilterList/mFilter.vue

@@ -0,0 +1,349 @@
+<template>
+  <view>
+    <view @tap="handleOpen">
+      <slot></slot>
+    </view>
+    <uni-popup ref="popup">
+      <view class="popup-content" :style="props.popupStyle">
+        <view class="popup-content-label">
+          <view class="popup-content-label-item active">{{ label }}</view>
+        </view>
+        <view class="popup-content-body">
+          <view v-for="(arr, i) in showList" :key="i" class="popup-content-body-list">
+            <!-- <view class="popup-content-body-list-label" :class="active === i ? 'active' : ''">{{ arr.label }}</view> -->
+            <view class="popup-content-body-list-items">
+              <view class="content">
+                <view
+                  v-for="_arr in arr.data"
+                  :key="_arr[props.itemValue]"
+                  class="py-1 dFlex"
+                  :class="arr.choose === _arr[props.itemValue] || (Array.isArray(arr.choose) && arr.choose.includes(_arr[props.itemValue]))? 'active' : ''"
+                  @tap="handleNext(_arr, i)"
+                >
+                  {{ _arr[props.itemLabel] }}
+                  <uni-icons
+                    v-if="Array.isArray(arr.choose) && arr.choose.includes(_arr[props.itemValue])"
+                    type="checkmarkempty"
+                    color="#00B760"
+                    size="16"
+                  />
+                </view>
+              </view>
+            </view>
+          </view>
+        </view>
+        <view class="popup-content-footer">
+          <button class="btn cancel" @tap="reset">重置</button>
+          <button class="btn submit" @tap="submit">确定</button>
+        </view>
+      </view>
+    </uni-popup>
+  </view>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+const emit = defineEmits(['change', 'init'])
+const props = defineProps({
+  popupStyle: {
+    type: [String, Object],
+    default: ''
+  },
+  value: {
+    type: [String, Array, Number, Object],
+    default: null
+  },
+  items: {  // 树结构
+    type: Array,
+    default: () => []
+  },
+  label: {
+    type: String,
+    default: ''
+  },
+  multiple: {
+    type: Boolean,
+    default: false
+  },
+  itemLabel: {
+    type: String,
+    default: 'label'
+  },
+  itemValue: {
+    type: String,
+    default: 'value'
+  },
+  children: {
+    type: String,
+    default: 'children'
+  }
+})
+
+const popup = ref()
+const showList = ref([])
+
+watch(
+  () => props.items,
+  (val) => {
+    // 初始化赋值
+    showList.value = [{
+      choose: -1,
+      data: val
+    }]
+  },
+  {
+    immediate: true,
+    deep: true
+  }
+)
+
+watch(
+  () => props.value,
+  (val) => {
+    if (!val) {
+      showList.value = [{
+        choose: -1,
+        data: props.items
+      }]
+      return
+    }
+    // 单选 设置回显
+    if (!props.multiple) {
+      const item = findItem(val, props.items)
+      if (!item) {
+        showList.value = [{
+          choose: -1,
+          data: props.items
+        }]
+        return
+      }
+      showList.value = item.map(e => {
+        return {
+          choose: e[props.itemValue],
+          data: e.data
+        }
+      })
+      emit('init', item[item.length - 1][props.itemLabel])
+      return
+    }
+    if (!Array.isArray(val)) {
+      showList.value = [{
+        choose: -1,
+        data: props.items
+      }]
+      return
+    }
+    // 多选 设置回显
+    const arr = []
+    const label = []
+    val.forEach(e => {
+      const item = findItem(e, props.items)
+      if (!item) {
+        return
+      }
+      label.push(item[item.length - 1][props.itemLabel])
+      if (!arr.length) {
+        arr.push(...item.map((e, i) => {
+          return {
+            choose: i === item.length - 1 ? [e[props.itemValue]] : e[props.itemValue],
+            data: e.data
+          }
+        }))
+        return
+      }
+      arr[arr.length - 1].choose.push(item[item.length - 1][props.itemValue])
+    })
+    if (!arr.length) {
+      showList.value = [{
+        choose: -1,
+        data: props.items
+      }]
+      return
+    }
+    showList.value = arr
+    emit('init', label)
+  },
+  {
+    immediate: true,
+    deep: true
+  }
+)
+
+const findItem = (value, lists) => {
+  let level = -1
+  return check(value, lists)
+  function check (val, items, arr = []) {
+    let i = 0
+    level++
+    while (i < items.length) {
+      arr[level] = {
+        ...items[i],
+        data: items
+      }
+      if (items[i][props.itemValue] === val) {
+        return arr
+      }
+      if (items[i][props.children] && items[i][props.children].length > 0) {
+        const data = check(val, items[i][props.children], arr)
+        if (data) {
+          return data
+        }
+      }
+      i++
+    }
+    level--
+    return false
+  }
+}
+
+const handleOpen = () => {
+  popup.value.open('bottom')
+}
+
+const handleNext = (item, index) => {
+  const _i = index + 1
+  // active.value = _i
+  showList.value.splice(_i, showList.value.length - _i)
+  showList.value[index].label = item[props.itemLabel]
+  if (item[props.children] && item[props.children].length) {
+    showList.value[index].choose = item[props.itemValue]
+    showList.value.push({
+      choose: -1,
+      data: item[props.children]
+    })
+    return
+  }
+  if (!props.multiple) {
+    showList.value[index].choose = item[props.itemValue]
+    return
+  }
+  if (!Array.isArray(showList.value[index].choose)) {
+    showList.value[index].choose = [item[props.itemValue]]
+    return
+  }
+  if (!showList.value[index].choose.includes(item[props.itemValue])) {
+    showList.value[index].choose.push(item[props.itemValue])
+    return
+  }
+  const _index = showList.value[index].choose.indexOf(item[props.itemValue])
+  showList.value[index].choose.splice(_index, 1)
+}
+
+const reset = () => {
+  showList.value = [{
+    choose: -1,
+    data: props.items
+  }]
+  emit('change', props.multiple ? [] : null)
+  popup.value.close()
+}
+const submit = () => {
+  const item = showList.value[showList.value.length - 1]
+  if (item.choose === -1) {
+    const _item = showList.value[showList.value.length - 2]
+    if (!item) {
+      emit('change', props.multiple ? [] : null)
+      popup.value.close()
+      return
+    }
+    emit('change', props.multiple ? [_item.choose] : _item.choose, props.multiple ? showList.value[0].label : _item.label, item)
+    popup.value.close()
+    return
+  }
+  emit('change', item.choose, props.multiple ? showList.value[0].label : item.label, item)
+  popup.value.close()
+}
+
+</script>
+
+<style lang="scss" scoped>
+.popup-content {
+  height: 580px;
+  background: #FFF;
+  border-radius: 20rpx 20rpx 0 0;
+  padding: 10px;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  &-label {
+    height: 50px;
+    display: flex;
+    font-size: 32rpx;
+    font-weight: bold;
+    margin-bottom: 10px;
+    border-bottom: 2rpx solid #EEE;
+    &-item {
+      display: flex;
+      align-items: center;
+      padding: 0 15px;
+      box-sizing: border-box;
+      &.active {
+        border-bottom: 2px solid #00B760;
+      }
+    }
+  }
+  &-body {
+    flex: 1;
+    height: 0;
+    display: flex;
+    &-list {
+      height: 100%;
+      min-width: 100px;
+      margin-right: 20px;
+      display: flex;
+      flex-direction: column;
+      &-label {
+        border-bottom: 1px solid #eee;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        height: 50px;
+        box-sizing: border-box;
+        margin-bottom: 10px;
+        &.active {
+          border-bottom: 2px solid #00B760;
+        }
+      }
+      &-items {
+        flex: 1;
+        height: 0;
+        .content {
+          height: 100%;
+          overflow-x: hidden;
+          overflow-y: auto;
+          .active {
+            color: #00B760;
+          }
+        }
+      }
+    }
+  }
+  &-footer {
+    height: 50px;
+    display: flex;
+    align-items: center;
+    .btn {
+      font-size: 28rpx;
+      height: 30px;
+      line-height: 30px;
+      &.cancel {
+        width: 40%;
+        margin-right: 10px;
+      }
+      &.submit {
+        flex: 1;
+        background: #00B760;
+        color: #FFF ;
+      }
+    }
+  }
+}
+.dFlex {
+  display: flex;
+  justify-content: space-between;
+  height: 20px;
+}
+.py-1 {
+  padding: 10px 0;
+}
+</style>

+ 101 - 0
components/FilterList/select.vue

@@ -0,0 +1,101 @@
+<template>
+  <m-filter
+    :label="props.label"
+    :items="props.items"
+    :item-label="props.itemLabel"
+    :item-value="props.itemValue"
+    :multiple="props.multiple"
+    :value="showValue"
+    @change="handleChange"
+    @init="handleInit"
+  >
+    <view class="content">
+      <view class="content-cover"></view>
+      <uni-easyinput v-model="showLabel" placeholder="请选择其它感兴趣的城市" :clearable="false"/>
+    </view>
+  </m-filter>
+</template>
+
+<script setup>
+import MFilter from './mFilter'
+import { ref, watch } from 'vue'
+
+const emit = defineEmits(['update:modelValue'])
+const props = defineProps({
+  modelValue: { // 传入值
+    type: [String, Number, Array, Object],
+    default: ''
+  },
+  label: {
+    type: String,
+    default: '请选择'
+  },
+  items: {
+    type: Array,
+    default: () => []
+  },
+  itemLabel: {
+    type: String,
+    default: 'label'
+  },
+  itemValue: {
+    type: String,
+    default: 'value'
+  },
+  multiple: {
+    type: Boolean,
+    default: false
+  }
+})
+// 回显使用id
+const showValue = ref(props.modelValue)
+const showLabel = ref(null)
+
+// const isChange = ref(false)
+watch(
+  () => props.modelValue,
+  (val) => {
+    showValue.value = val
+  },
+  {
+    immediate: true,
+    deep: true
+  }
+)
+
+const handleInit = (val) => {
+  showLabel.value = val
+  console.log('label', val)
+}
+
+const handleChange = (value, label, item) => {
+  console.log(111)
+  if (props.multiple) {
+    if (!value.length) {
+      showLabel.value = null
+      emit('update:modelValue', null)
+      return
+    }
+    showLabel.value = item.data.filter(e => value.includes(e[props.itemValue])).map(e => e[props.itemLabel])
+    emit('update:modelValue', value)
+    return
+  }
+  showLabel.value = label
+  emit('update:modelValue', value)
+}
+
+</script>
+
+<style lang="scss" scoped>
+.content {
+	position: relative;
+	&-cover {
+		position: absolute;
+		width: 100%;
+		height: 100%;
+		left: 0;
+		top: 0;
+		z-index: 3;
+	}
+}
+</style>

+ 49 - 0
components/Navbar/index.vue

@@ -0,0 +1,49 @@
+<template>
+  <view class="navbar-box" :style="{'height': (navbarHeight < defaultLogoHeight ? (defaultLogoHeight + 10) : navbarHeight) + 'px', 'paddingTop': statusBarHeight + 'px'}">
+    <image src="https://minio.citupro.com/dev/menduner/poster.png" class="navbar-box-logo" :style="{'height': defaultLogoHeight + 'px'}"></image>
+    <!-- <view class="navbar-box-title">{{ title }}</view> -->
+  </view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+
+defineProps({
+  title: {
+    type: String,
+    default: '门墩儿'
+  }
+})
+
+const defaultLogoHeight = 45
+const navbarHeight = ref(0)
+const statusBarHeight = ref(0)
+onLoad(() => {
+  const systemInfo = uni.getSystemInfoSync()
+  statusBarHeight.value = systemInfo.statusBarHeight
+  const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
+  navbarHeight.value = menuButtonInfo.height + ((menuButtonInfo.top - statusBarHeight.value) * 2) + 15
+})
+</script>
+
+<style scoped lang="scss">
+.navbar-box {
+  width: 100%;
+  position: relative;
+  background: linear-gradient(90deg, #66BB6A 0, #64FFDA 100%);
+  &-logo {
+    position: absolute;
+    left: 50%;
+    transform: translateX(-50%);
+    width: 100px;
+  }
+  // &-title {
+  //   position: absolute;
+  //   top: 50%;
+  //   left: 50%;
+  //   transform: translate(-30%, 0);
+  //   color: #fff;
+  // }
+}
+</style>

+ 142 - 76
components/PositionList/index.vue

@@ -1,92 +1,133 @@
 <template>
-  <view v-if="list.length > 0">
-    <view v-for="(item, index) in list" :key="index">
-      <!-- 职位信息 -->
-      <view class="list-shape" @click="toDetail(1, item)">
-        <!-- 职位 -->
-        <view class="titleBox my-5">
-          <view style="display: flex;align-items: center;">
-            <view v-if="item.job?.hire" class="iconfont icon-a-1_zhaopin ss-m-r-10" style="color: #e03506; font-size: 25px;"></view>
-            <!-- <view v-if="item.job.name.indexOf('style')" v-html="item.job.name" class="job-name" style=""></view> -->
-            <rich-text v-if="item.job?.name?.indexOf('style')" class="job-name" :nodes="item.job.name"></rich-text>
-            <view v-else class="job-name" style="">{{item.job?.name}}</view>
-          </view>
-          <span class="salary-text">{{ item.job?.payFrom }}-{{ item.job?.payTo }}{{ item.job?.payName ? '/' + item.job?.payName : '' }}</span>
-        </view>
-        <!-- 工作地 -->
-        <view style="font-size: 13px;" class="mt">
-          <span class="tag-gap">
-            <span>{{item.job?.areaName }}</span>
-            <span class="divider-mx" v-if="item.job?.areaName && item.job?.eduName">|</span>
-            <span>{{item.job?.eduName }}</span>
-            <span class="divider-mx" v-if="item.job?.expName">|</span>
-            <span>{{item.job?.expName }}</span>
-          </span>
-        </view>
-        <!-- 岗位tag  -->
-        <view class="mt" v-if="showWelfareTag">
-          <uni-tag 
-            v-for="(tag,i) in item.enterprise?.welfareList || []"
-            :key="i"
-            class="tag-gap"
-            :text="tag"
-            inverted="false"
-            size="mini"
-            custom-style="background-color: #ececec;color:#666;border-color:#ececec;display: inline-block;"
-          />
-        </view>
-        <view style="text-align: end;" v-if="item.job?.hire">
-          <uni-tag
-            class="ss-m-l-10"
-            v-if="item?.job?.hirePrice && item?.job?.hirePrice > 0" 
-            :text="`赏金:${commissionCalculation(item.job.hirePrice, 1)}元`"
-            inverted="false"
-            size="default"
-            custom-style="background-color: #e2f0ef; color:#00897B; border-color:#e2f0ef;"
-          />
-        </view>
+  <view class="ss-m-x-20">
+    <!-- 招聘会 -->
+    <view v-if="props.showJobFairEntrance" class="ss-p-t-20" style="position: relative" @click="handleToJobFair">
+      <image
+        src="https://minio.menduner.com/dev/menduner/miniProgram/Grand-Mercure.jpg"
+        style="width: 100%; height: 100px; border-radius: 8px"
+      ></image>
+      <view
+        style="position: absolute; top: 20rpx; width: 100%; text-align: center; height: 100px; line-height: 100px; font-size: 46px; color: #fff; font-weight: bold;"
+      >
+        招 聘 会
       </view>
-      <!-- 企业信息 -->
-      <view class="sub-li-bottom">
-        <view class="avatarBox" @click="toDetail(0, item)">
-          <image class="enterAvatar ml" :src="item.enterprise?.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'"></image>
+    </view>
+    <!-- 岗位列表 -->
+    <view v-if="list.length > 0" class="ss-p-b-30 ss-p-t-20">
+      <view v-for="(item, index) in list" :key="index" class="mList" :class="{ 'disable': !jobFairId && item.job?.status === '1'}" @click="toDetail(item)">
+        <!-- 职位信息 -->
+        <view class="list-shape" :style="`border-radius: ${props.showEntInfo ? '12px 12px 0 0' : '12px'};`">
+          <!-- 职位 -->
+          <view class="titleBox my-5">
+            <view style="display: flex;align-items: center;">
+              <view v-if="item.job?.hire" class="iconfont icon-a-1_zhaopin ss-m-r-10" style="color: #e03506; font-size: 25px;"></view>
+              <image v-if="props.jobFairId" src="/static/svg/jobFair.svg" class=" ss-m-r-10" style="width: 20px; height: 20px;"></image>
+              <rich-text v-if="item.job?.name?.indexOf('style') !== -1" class="job-name" :nodes="item.job.name"></rich-text>
+              <view v-else class="job-name">{{ formatName(item.job?.name) }}</view>
+            </view>
+          </view>
+          <!-- 薪酬、工作地、学历、工作经验 -->
+          <view class="d-flex align-center justify-space-between">
+            <view class="font-size-13 ellipsis" :style="{'max-width': !item.job?.payFrom && !item.job?.payTo ? '78%' : '56%'}">
+              <span class="tag-gap" style="color: #808080;">
+                <span>{{item.job?.area?.str ?? '全国' }}</span>
+                <span class="divider-mx" v-if="item.job?.eduName">|</span>
+                <span>{{item.job?.eduName }}</span>
+                <span class="divider-mx" v-if="item.job?.expName">|</span>
+                <span>{{item.job?.expName }}</span>
+              </span>
+            </view>
+            <view>
+              <span v-if="!item.job?.payFrom && !item.job?.payTo" class="salary-text">面议</span>
+              <span v-else class="salary-text">{{ item.job?.payFrom }}-{{ item.job?.payTo }}{{ item.job?.payName ? '/' + item.job?.payName : '' }}</span>
+            </view>
+          </view>
+          <!-- 岗位tag  -->
+          <view class="mt" v-if="showWelfareTag">
+            <uni-tag 
+              v-for="(tag,i) in item.job?.tagList || []"
+              :key="i"
+              class="tag-gap"
+              :text="tag"
+              inverted="false"
+              size="mini"
+              custom-style="background-color: #ececec;color:#666;border-color:#ececec;display: inline-block;"
+            />
+          </view>
+          <view style="text-align: end;" v-if="item.job?.hire">
+            <uni-tag
+              class="ss-m-l-10"
+              v-if="item?.job?.hirePrice && item?.job?.hirePrice > 0" 
+              :text="`赏金:${commissionCalculation(item.job.hirePrice / 100, 1)}元`"
+              inverted="false"
+              size="default"
+              custom-style="background-color: #e2f0ef; color:#00B760; border-color:#e2f0ef;"
+            />
+          </view>
+          <view v-if="props.showUpdateTime" class="font-size-13 color-999 ss-m-t-10" :style="`text-align: ${props.updateTimeAlign};`">更新时间:{{ timesTampChange(item?.job?.refreshTime || item.job?.updateTime, 'Y-M-D h:m') }}</view>
         </view>
-        <view class="ss-m-l-35">
-          <!-- 企业简称 -->
-          <span
-            class="mr"
-            style="font-weight: bold;"
-            @click="toDetail(0, item)"
-          >
-            {{ item.enterprise?.anotherName || ' -- ' }}
-          </span>
-          <span>{{ item.enterprise?.industryName || '行业未知' }}</span>
-          <span class="divider tag-gap1"> | </span>
-          <span class="mr">{{ item.enterprise?.scaleName || '规模未知' }}</span>
+        <!-- 企业信息 -->
+        <view v-if="props.showEntInfo" class="sub-li-bottom" @tap="handleClickEnt(item)">
+          <view class="avatarBox">
+            <image class="enterAvatar ml" :src="item.enterprise?.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'"></image>
+          </view>
+          <view class="ss-m-l-35">
+            <view class="mr">{{ formatName(item.enterprise?.anotherName || item.enterprise.name) }}</view>
+            <span class="color-999">{{ item.enterprise?.industryName || '' }}</span>
+            <span class="divider tag-gap1" v-if="item.enterprise?.industryName && item.enterprise?.scaleName"> | </span>
+            <span class="mr color-999">{{ item.enterprise?.scaleName || '' }}</span>
+          </view>
         </view>
       </view>
+      <view v-if="props.noMore" class="noMore">暂无更多数据</view>
+    </view>
+    <view v-else>
+      <image src="https://minio.citupro.com/dev/static/nodata.png" mode="widthFix" style="width: 100vw;height: 100vh;"></image>
+      <view style="color: gray; text-align: center;">暂无数据</view>
     </view>
-    <view v-if="props.noMore" class="noMore">暂无更多数据</view>
   </view>
 </template>
+
 <script setup>
 import { commissionCalculation } from '@/utils/position'
-// import { ref } from 'vue'
+import { timesTampChange } from '@/utils/date'
+import { formatName } from '@/utils/getText'
+const emit = defineEmits(['entClick'])
+
 const props = defineProps({
   list: { type: Array, default: () => [] },
+  jobFairId: { type: [String, Number], default: '' }, // 招聘会id
+  showJobFairEntrance: { type: Boolean, default: false }, // 招聘会
+  showEntInfo: { type: Boolean, default: true },
+  updateTimeAlign: { type: String, default: 'end' },
+  showUpdateTime: { type: Boolean, default: true },
   noMore: { type: Boolean, default: false },
   showWelfareTag: { type: Boolean, default: true }
 })
 
 //岗位详情
-const toDetail = (isPosition, item) =>{
-  const url = isPosition
-    ? `/pagesB/positionDetail/index?id=${item.job?.id}`
-    : `/pagesB/companyDetail/index?id=${item.enterprise?.id}`
-  //
+const toDetail = (item) =>{
+  if (!item?.job?.id) return
+  let url = `/pagesB/positionDetail/index?id=${item.job.id}&area=${item.job.areaName}`
+  if (props.jobFairId) url += `&jobFairId=${props.jobFairId}`
   uni.navigateTo({ url })
 }
 
+//招聘会
+const handleToJobFair = () => {
+	uni.navigateTo({
+		url: '/pagesB/jobFair/index'
+	})
+}
+
+const handleClickEnt = (item) => {
+  const info = {
+    enterpriseId: item?.enterprise?.id || null,
+    anotherName: item?.enterprise?.anotherName || null
+  }
+  emit('entClick', info)
+}
+
 </script>
 
 <style scoped lang="scss">
@@ -109,15 +150,15 @@ const toDetail = (isPosition, item) =>{
 .enterAvatar{
 	width: 40px;
 	height: 40px;
-	border-radius: 50%;
+	// border-radius: 50%;
 	margin: auto;
 }
 
 .job-name {
   font-size: 16px;
   font-weight: 700;
-  color: black;
-  max-width: 50vw;
+  color: #0E100F;
+  max-width: 80vw;
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
@@ -126,9 +167,10 @@ const toDetail = (isPosition, item) =>{
 .sub-li-bottom {
   display: flex;
   align-items: center;
-  background: linear-gradient(90deg, #f5fcfc 0, #fcfbfa 100%);
+  background-color: #fff;
   font-size: 13px;
   padding: 5px;
+  border-radius: 0 0 12px 12px;
   .avatarBox {
     max-width: 40px;
     max-height: 40px;
@@ -137,12 +179,13 @@ const toDetail = (isPosition, item) =>{
 
 .salary-text {
 	float: right;
-	color: #fe574a;
+	color: #00B760;
+  font-weight: 700;
 }
 .list-shape {
 	padding: 10px 30rpx 10px;
-  margin-top: 10px;
   background-color: #fff;
+  border-radius: 12px 12px 0 0;
   .titleBox {
     display: flex;
     align-items: center;
@@ -183,12 +226,35 @@ const toDetail = (isPosition, item) =>{
 	overflow: hidden;
 	text-overflow: ellipsis;
 }
+.mList {
+  margin-bottom: 20rpx;
+}
 /* 列表触底暂无更多 */
 .noMore{ text-align:center; color:grey; }
-.mt { margin-top: 10rpx; }
+.mt { margin-top: 15rpx; }
 .mb { margin-bottom: 10rpx; }
 .ml { margin-left: 20rpx; }
 .mr { margin-right: 20rpx; }
 .mr-10{ margin-right: 10rpx; }
 .my-5{ margin: 5px 0; }
+.disable {
+  position: relative;
+  overflow: hidden;
+  &::after {
+    content: '已失效';
+    position: absolute;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 1.2em;
+    font-weight: bold;
+    color: #fc796f;
+    top: 0;
+    border-radius: 12px;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(255, 255, 255, 0.75);
+  }
+}
 </style>

+ 94 - 0
components/ResumeStatus/index.vue

@@ -0,0 +1,94 @@
+<template>
+	<slot name="header"></slot>
+	<view class="content">
+		<view v-for="(item,index) in items" :key="item.label" class="content-box" @tap="handleTo(item)">
+			<view class="content-box-value">
+				{{ item.count }}
+			</view>
+			<view class="content-box-title">
+				{{ item.label }}
+			</view>
+		</view>
+	</view>
+	
+</template>
+
+<script setup>
+import { ref, watch } from 'vue';
+import { getRecommendCount } from '@/api/position.js'
+import { getDict } from '@/hooks/useDictionaries.js'
+import { onShow } from '@dcloudio/uni-app'
+// const props = defineProps({
+// 	type: {
+// 		type: String,
+// 		default: 'menduner_hire_job_cv_status'
+// 	}
+// })
+
+import { userStore } from '@/store/user'
+const useUserStore = userStore()
+
+// 监听登录状态
+watch(() => useUserStore.refreshToken, (newVal, oldVal) => {
+	const reset = Boolean(!newVal)
+	recommendCount(reset)
+})
+
+onShow(() => {
+	recommendCount()
+})
+
+const items = ref([])
+
+const handleTo = (item) => {
+	uni.navigateTo({
+		url: `/pagesA/recommendation/index?id=${item.value}`
+	})
+}
+
+async function recommendCount (reset = false) {
+	try {
+		const { data: dict } = await getDict('menduner_hire_job_cv_status')
+		if (!dict?.data) {
+			return
+		}
+		items.value = dict.data.map(e => {
+			return {
+				...e,
+				count: 0
+			}
+		})
+		// console.log(items)
+		if (reset) {
+			return
+		}
+		const { data } = await getRecommendCount()
+		if (!data) {
+			return
+		}
+		items.value.forEach(e => {
+			e.count = data.find(_e => _e.key === e.value)?.value || 0
+		})
+	} catch (error) {
+		// console.log(error)
+	}
+}
+
+</script>
+
+<style scoped lang="scss">
+.content {
+	display: flex;
+	justify-content: space-around;
+	padding: 36rpx 12rpx;
+	&-box {
+		font-size: 24rpx;
+		color: #999;
+		text-align: center;
+		&-value {
+			font-size: 1.8em;
+			color: #000;
+		}
+	}
+}
+</style>

+ 15 - 11
components/SwiperAd/index.vue

@@ -1,5 +1,5 @@
 <template>
-	<view v-if="!props.hide" :style="`width: ${props.width}; height: ${props.height};`">
+	<view v-if="!props.hide" :style="`height: ${props.height}; margin: ${props.margin};`">
 		<view v-if="props.list?.length">
 			<swiper
         class="swiper"
@@ -10,21 +10,22 @@
         :duration="props.duration"
         indicator-active-color="#fff"
       >
-				<swiper-item v-for="(item, index) in list" :key="'swiperItem'+index">
-					<view>
-            <image
-              :mode="strType ? props.mode : item.mode"
-              :src="strType ? item : item[props.imgUrlKey]"
-               :style="`width: ${props.width}; height: ${props.height};`"
-              @error="imageError"
-            ></image>
-          </view>
+				<swiper-item v-for="(item, index) in list" :key="'swiperItem'+index" :style="`border-radius: ${props.borderRadius};`">
+          <image
+            :mode="props.mode"
+            :src="strType ? item : item[props.imgUrlKey]"
+             :style="`width: ${props.width}; height: ${props.height};`"
+            @error="imageError"
+            @click="emit('click', item)"
+          ></image>
 				</swiper-item>
 			</swiper>
 		</view>
 	</view>
 </template>
+
 <script setup>
+const emit = defineEmits(['click'])
 const props = defineProps({
   list: { type: Array, default: () => [] },
   indicatorDots: { type: Boolean, default: true }, // 是否显示面板指示点
@@ -33,10 +34,13 @@ const props = defineProps({
   duration: { type: Number, default: 500 }, // 滑动动画时长
   strType: { type: Boolean, default: true }, // 数组类型或者对象类型
   imgUrlKey: { type: String, default: 'src' },
-  mode: { type: String, default: 'aspectFill' }, // 图片裁剪、缩放的模式。aspectFill保持纵横比缩放图片,只保证图片的短边能完全显示出来
+  mode: { type: String, default: 'scaleToFill' }, // 图片裁剪、缩放的模式。aspectFill保持纵横比缩放图片,只保证图片的短边能完全显示出来
   hide: { type: Boolean, default: false }, // 隐藏
   width: { type: String, default: '100%' },
   height: { type: String, default: '150px' },
+  borderRadius: { type: String, default: '10px' },
+  borderRadius: { type: String, default: '10px' },
+  margin: { type: String, default: '10px' },
 })
 
 const imageError = (e) => {

+ 345 - 0
components/searchCombox/index.vue

@@ -0,0 +1,345 @@
+<template>
+	<view class="uni-combox" :class="border ? '' : 'uni-combox__no-border'">
+		<view class="uni-combox__input-box">
+			<input class="uni-combox__input" type="text" :placeholder="placeholder" placeholder-class="uni-combox__input-plac"
+				v-model="inputVal" @input="onInput" @focus="onFocus" @blur="onBlur" />
+			<uni-icons :type="showSelector? 'top' : 'bottom'" size="14" color="#999" @click="toggleSelector"></uni-icons>
+		</view>
+		<view class="uni-combox__selector" v-if="showSelector">
+			<view class="uni-popper__arrow"></view>
+			<scroll-view scroll-y="true" class="uni-combox__selector-scroll">
+				<view class="uni-combox__selector-empty" v-if="filterCandidatesLength === 0">
+					<text>{{emptyTips}}</text>
+				</view>
+				<view
+          class="uni-combox__selector-item"
+          v-for="(item,index) in filterCandidates" :key="index"
+          :style="item[valueKey] == dictVal ? 'font-weight: bold;background-color: ' + selectedBackground + ';color: ' + selectedColor : ''"
+					@click="onSelectorClick(index)"
+        >
+					<text>{{item[`${labelKey}`]}}</text>
+				</view>
+			</scroll-view>
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * Combox 组合输入框
+	 * @description 组合输入框一般用于既可以输入也可以选择的场景
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=1261
+	 * @property {String} label 左侧文字
+	 * @property {String} labelWidth 左侧内容宽度
+	 * @property {String} placeholder 输入框占位符
+	 * @property {Array} candidates 候选项列表
+	 * @property {String} emptyTips 筛选结果为空时显示的文字
+	 * @property {String} value 组合框的值
+	 */
+  const _this = this
+  export default {
+		name: 'uniCombox',
+		emits: ['input', 'update:modelValue', 'onBlur'],
+		props: {
+			border: {
+				type: Boolean,
+				default: true
+			},
+			label: {
+				type: String,
+				default: ''
+			},
+			labelWidth: {
+				type: String,
+				default: 'auto'
+			},
+			keyValue: {
+				type: String,
+				default: ''
+			},
+			placeholder: {
+				type: String,
+				default: ''
+			},
+			candidates: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			emptyTips: {
+				type: String,
+				default: '无匹配项'
+			},
+			itemTextName: {
+				type: String,
+				default: 'label'
+			},
+			itemValueName: {
+				type: String,
+				default: 'value'
+			},
+			labelKey: {
+				type: String,
+				default: 'text'
+			},
+			valueKey: {
+				type: String,
+				default: 'value'
+			},
+			// #ifndef VUE3
+			value: {
+				type: [String, Number],
+				default: ''
+			},
+			// #endif
+			// #ifdef VUE3
+			modelValue: {
+				type: [String, Number],
+				default: ''
+			},
+      // 被选中的背景颜色
+      selectedBackground: {
+        type: String,
+        default: '#daf3e6',
+      },
+      // 被选中的字体颜色
+      selectedColor: {
+        type: String,
+        default: '#00B760',
+      },
+			// #endif
+		},
+
+		data() {
+			return {
+				showSelector: false,
+				inputVal: '', //显示
+				dictVal: '', // id
+				filterCandidates: []
+			}
+		},
+		computed: {
+			labelStyle() {
+				if (this.labelWidth === 'auto') {
+					return ''
+				}
+				return `width: ${this.labelWidth}`
+			},
+			// 为了点击选择能够显示所有选项,把这个filterCandidates放在data中
+			// filterCandidates() {
+			// 	return this.candidates.filter((item) => {
+			// 		return item[`${this.labelKey}`].toString().indexOf(this.inputVal) > -1
+			// 	})
+			// },
+			filterCandidatesLength() {
+				return this.filterCandidates.length
+			}
+		},
+		watch: {
+			//默认值
+			keyValue: {
+				handler(newVal) {
+					this.inputVal = newVal
+				},
+				immediate: true
+			},
+			// #ifndef VUE3
+			value: {
+				handler(newVal) {
+					this.dictVal = newVal
+				},
+				immediate: true
+			},
+			// #endif
+			// #ifndef VUE3
+			// 因为获取列表是个异步的过程,需要对列表进行监听
+			candidates: function(arr) {
+				this.setLabel()
+				this.filterCandidates = arr.filter(item => {
+					return item[`${this.labelKey}`].toString().indexOf(this.inputVal) > -1
+				})
+			},
+			// #endif
+			// #ifdef VUE3
+			modelValue: {
+				handler(newVal) {
+					this.inputVal = newVal
+					this.dictVal = newVal
+					this.setLabel()
+				},
+				immediate: true,
+				deep: true
+			},
+			// #endif
+		},
+		methods: {
+			toggleSelector() {
+				this.showSelector = !this.showSelector
+			},
+			onFocus() {
+				this.filterCandidates = this.candidates
+				this.showSelector = true
+			},
+			onBlur() {
+				setTimeout(() => {
+					this.showSelector = false
+          this.$emit('onBlur')
+				}, 153)
+			},
+			onSelectorClick(index) {
+				this.dictVal = this.filterCandidates[index][`${this.valueKey}`]
+				//this.dictVal 的赋值一定要在this.inputVal前执行,
+				//因为this.filterCandidates会监听this.inputVal的变化被重新赋值
+				//这样在选择列表中非第一个选项会报错
+				this.inputVal = this.filterCandidates[index][`${this.labelKey}`]
+				this.showSelector = false
+				this.$emit('input', this.dictVal)
+				this.$emit('update:modelValue', this.dictVal)
+			},
+			onInput() {
+				this.filterCandidates = this.candidates.filter(item => {
+					return item[`${this.labelKey}`].toString().indexOf(this.inputVal) > -1
+				})
+				setTimeout(() => {
+          if (!this.inputVal) this.dictVal = this.inputVal
+					this.$emit('input', this.inputVal)
+					this.$emit('update:modelValue', this.inputVal)
+				})
+			},
+			setLabel() {
+				this.$nextTick(()=>{
+					if (this.candidates?.length > 0 && this.dictVal) {
+						const obj = this.candidates.find(item => item[`${this.valueKey}`] === this.dictVal)
+						if (obj) {
+							this.inputVal = obj[`${this.labelKey}`]
+						}
+					}
+				})
+			},
+			getOnInputValue() {
+        return this.inputVal
+			},
+			getValue() {
+				if (this.inputVal === this.dictVal) return { [this.itemTextName]: this.inputVal, [this.itemValueName]: null }
+				else return { [this.itemTextName]: null, [this.itemValueName]: this.dictVal }
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.uni-combox {
+		position: relative;
+		display: flex;
+		flex-direction: row;
+		align-items: center;
+		min-height: 70rpx;
+		// width: 100%;
+		padding: 0 15rpx 0 20rpx;
+  	font-size: 28rpx;
+		border: 1px solid #e5e5e5;
+		border-radius: 8rpx;
+		// background-color: #fff;
+	}
+
+	.uni-combox__label {
+		padding-right: 10rpx;
+		color: #999999;
+		font-size: 32rpx;
+		line-height: 44rpx;
+	}
+
+	.uni-combox__input-box {
+		position: relative;
+		display: flex;
+		flex: 1;
+		flex-direction: row;
+		align-items: center;
+	}
+
+	.uni-combox__input {
+		flex: 1;
+		height: 44rpx;
+		font-size: 28rpx;
+		line-height: 44rpx;
+	}
+
+	.uni-combox__input-plac {
+		color: #999;
+		font-size: 12px;
+	}
+
+	.uni-combox__selector {
+		position: absolute;
+		top: calc(100% + 12px);
+		left: 0;
+		z-index: 999;
+		box-sizing: border-box;
+		width: 100%;
+		padding: 8rpx 0;
+		background-color: #FFFFFF;
+		border: 1px solid #ebeef5;
+		border-radius: 12rpx;
+		box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+	}
+
+	.uni-combox__selector-scroll {
+		max-height: 300rpx;
+		box-sizing: border-box;
+	}
+
+	.uni-combox__selector-empty,
+	.uni-combox__selector-item {
+		display: flex;
+		padding: 0px 0px;
+		// padding: 0px 10px;
+		font-size: 14px;
+  	line-height: 70rpx;
+		text-indent: 1rem;
+		text-align: center;
+		cursor: pointer;
+	}
+
+	.uni-combox__selector-item:hover {
+		background-color: #e5e5e5;
+	}
+
+	.uni-combox__selector-empty:last-child,
+	.uni-combox__selector-item:last-child {
+		border-bottom: none;
+	}
+
+	// picker 弹出层通用的指示小三角
+	.uni-popper__arrow,
+	.uni-popper__arrow::after {
+		position: absolute;
+		display: block;
+		width: 0;
+		height: 0;
+		border-color: transparent;
+		border-style: solid;
+		border-width: 12rpx;
+	}
+
+	.uni-popper__arrow {
+		top: -12rpx;
+		left: 10%;
+		margin-right: 3px;
+		border-top-width: 0;
+		border-bottom-color: #ebeef5;
+		filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
+	}
+
+	.uni-popper__arrow::after {
+		content: " ";
+		top: 2rpx;
+		margin-left: -12rpx;
+		border-top-width: 0;
+		border-bottom-color: #fff;
+	}
+
+	.uni-combox__no-border {
+		border: none;
+	}
+</style>

+ 79 - 0
components/studentDeliveryForm/index.vue

@@ -0,0 +1,79 @@
+<template>
+	<view class="f-straight">
+    <uni-forms ref="form" :modelValue="formData" :rules="rules" validateTrigger="bind" :label-width="formLabelWidth">
+      <uni-forms-item label="实习到岗开始时间" name="startTime" required>
+				<picker mode="date" :value="formData.startTime" fields="month" @change="e => formData.startTime = e.detail.value">
+					<view v-if="formData.startTime" class="pickerText">{{ formData.startTime }}</view>
+					<view v-else class="pickerText">请选择</view>
+				</picker>
+			</uni-forms-item>
+      <uni-forms-item label="实习到岗结束时间" name="endTime" required>
+				<picker mode="date" :value="formData.endTime" fields="month" @change="e => formData.endTime = e.detail.value">
+					<view v-if="formData.endTime" class="pickerText">{{ formData.endTime }}</view>
+					<view v-else class="pickerText">请选择</view>
+				</picker>
+			</uni-forms-item>
+    </uni-forms>
+	</view>
+</template>
+
+<script setup>
+import { ref, unref } from 'vue'
+import { convertYearMonthToTimestamp } from '@/utils/date.js'
+
+const props = defineProps({
+  formLabelWidth: { type: [String, Number], default: '136px' },
+})
+
+const form = ref()
+let formData = ref({
+  startTime: '',
+  endTime: ''
+})
+
+const rules = {
+	startTime:{
+		rules: [{required: true, errorMessage: '请选择实习到岗开始时间' }]
+	},
+	endTime:{
+		rules: [{required: true, errorMessage: '请选择实习到岗结束时间' }]
+	}
+}
+
+// 保存
+const getQueryParams = async () => {
+  const valid = await unref(form).validate()
+  if (!valid) return
+  const startTime = convertYearMonthToTimestamp(formData.value.startTime)
+  const endTime = convertYearMonthToTimestamp(formData.value.endTime)
+  if (rules?.startTime && !startTime) {
+    uni.showToast({ icon: 'none', title: '请选择实习到岗开始时间' })
+    return
+  }
+  if (rules?.endTime && !endTime) {
+    uni.showToast({ icon: 'none', title: '请选择实习到岗结束时间' })
+    return
+  }
+  if (startTime > endTime) {
+    uni.showToast({ icon: 'none', title: '开始时间不能大于结束时间' })
+    return
+  }
+  return { practiceStartTime: startTime, practiceEndTime: endTime }
+}
+
+defineExpose({
+	getQueryParams
+})
+</script>
+
+<style scoped lang="scss">
+.f-straight{
+	display: flex;
+	justify-content: center;
+	flex-direction: column;
+}
+.pickerText {
+  height: 36px;
+  line-height: 36px;
+}
+</style>

+ 586 - 0
components/ui/ct-popup/index.vue

@@ -0,0 +1,586 @@
+<template>
+  <view
+    v-if="showPopup"
+    class="uni-popup"
+    :class="[popupstyle, isDesktop ? 'fixforpc-z-index' : '']"
+    :style="[{ zIndex: zIndex }]"
+    @touchmove.stop.prevent="clear"
+  >
+    <view @touchstart="touchstart">
+      <uni-transition
+        key="1"
+        v-if="maskShow"
+        name="mask"
+        mode-class="fade"
+        :styles="maskClass"
+        :duration="duration"
+        :show="showTrans"
+        @click="onTap"
+      />
+      <uni-transition
+        key="2"
+        :mode-class="ani"
+        name="content"
+        :styles="{ ...transClass, ...borderRadius }"
+        :duration="duration"
+        :show="showTrans"
+        @click="onTap"
+      >
+        <view
+          v-if="showPopup"
+          class="uni-popup__wrapper"
+          :style="[{ backgroundColor: bg }, borderRadius]"
+          :class="[popupstyle]"
+          @click="clear"
+        >
+          <uni-icons
+            v-if="showClose"
+            class="close-icon"
+            color="#F6F6F6"
+            type="closeempty"
+            size="32"
+            @click="close"
+          ></uni-icons>
+          <slot />
+        </view>
+      </uni-transition>
+    </view>
+    <!-- #ifdef H5 -->
+    <keypress v-if="maskShow" @esc="onTap" />
+    <!-- #endif -->
+  </view>
+  <!-- #ifdef MP -->
+  <view v-else style="display: none">
+    <slot></slot>
+  </view>
+  <!-- #endif -->
+</template>
+
+<script>
+  // #ifdef H5
+  import keypress from './keypress.js';
+  // #endif
+
+  /**
+   * PopUp 弹出层
+   * @description 弹出层组件,为了解决遮罩弹层的问题
+   * @tutorial https://ext.dcloud.net.cn/plugin?id=329
+   * @property {String} type = [top|center|bottom|left|right|message|dialog|share] 弹出方式
+   * 	@value top 顶部弹出
+   * 	@value center 中间弹出
+   * 	@value bottom 底部弹出
+   * 	@value left		左侧弹出
+   * 	@value right  右侧弹出
+   * 	@value message 消息提示
+   * 	@value dialog 对话框
+   * 	@value share 底部分享示例
+   * @property {Boolean} animation = [true|false] 是否开启动画
+   * @property {Boolean} maskClick = [true|false] 蒙版点击是否关闭弹窗(废弃)
+   * @property {Boolean} isMaskClick = [true|false] 蒙版点击是否关闭弹窗
+   * @property {String}  backgroundColor 主窗口背景色
+   * @property {String}  maskBackgroundColor 蒙版颜色
+   * @property {Boolean} safeArea		   是否适配底部安全区
+   * @event {Function} change 打开关闭弹窗触发,e={show: false}
+   * @event {Function} maskClick 点击遮罩触发
+   */
+
+  export default {
+    name: 'SuPopup',
+    components: {
+      // #ifdef H5
+      keypress,
+      // #endif
+    },
+    emits: ['change', 'maskClick', 'close'],
+    props: {
+      // 开启状态
+      show: {
+        type: Boolean,
+        default: false,
+      },
+      // 顶部,底部时有效
+      space: {
+        type: Number,
+        default: 0,
+      },
+      // 默认圆角
+      round: {
+        type: [String, Number],
+        default: 0,
+      },
+      // 是否显示关闭
+      showClose: {
+        type: Boolean,
+        default: false,
+      },
+      // 开启动画
+      animation: {
+        type: Boolean,
+        default: true,
+      },
+      // 弹出层类型,可选值,top: 顶部弹出层;bottom:底部弹出层;center:全屏弹出层
+      // message: 消息提示 ; dialog : 对话框
+      type: {
+        type: String,
+        default: 'bottom',
+      },
+      // maskClick
+      isMaskClick: {
+        type: Boolean,
+        default: null,
+      },
+      // TODO 2 个版本后废弃属性 ,使用 isMaskClick
+      maskClick: {
+        type: Boolean,
+        default: null,
+      },
+      // 可设置none
+      backgroundColor: {
+        type: String,
+        default: '#ffffff',
+      },
+      backgroundImage: {
+        type: String,
+        default: '',
+      },
+      safeArea: {
+        type: Boolean,
+        default: true,
+      },
+      maskBackgroundColor: {
+        type: String,
+        default: 'rgba(0, 0, 0, 0.4)',
+      },
+      zIndex: {
+        type: [String, Number],
+        default: 10075,
+      },
+    },
+
+    watch: {
+      show: {
+        handler: function (newValue, oldValue) {
+          if (typeof oldValue === 'undefined' && !newValue) {
+            return;
+          }
+          if (newValue) {
+            this.open();
+          } else {
+            this.close();
+          }
+        },
+        immediate: true,
+      },
+      /**
+       * 监听type类型
+       */
+      type: {
+        handler: function (type) {
+          if (!this.config[type]) return;
+          this[this.config[type]](true);
+        },
+        immediate: true,
+      },
+      isDesktop: {
+        handler: function (newVal) {
+          if (!this.config[newVal]) return;
+          this[this.config[this.type]](true);
+        },
+        immediate: true,
+      },
+      /**
+       * 监听遮罩是否可点击
+       * @param {Object} val
+       */
+      maskClick: {
+        handler: function (val) {
+          this.mkclick = val;
+        },
+        immediate: true,
+      },
+      isMaskClick: {
+        handler: function (val) {
+          this.mkclick = val;
+        },
+        immediate: true,
+      },
+      // H5 下禁止底部滚动
+      showPopup(show) {
+        // #ifdef H5
+        // fix by mehaotian 处理 h5 滚动穿透的问题
+        document.getElementsByTagName('body')[0].style.overflow = show ? 'hidden' : 'visible';
+        // #endif
+      },
+    },
+    data() {
+      return {
+        duration: 300,
+        ani: [],
+        showPopup: false,
+        showTrans: false,
+        popupWidth: 0,
+        popupHeight: 0,
+        config: {
+          top: 'top',
+          bottom: 'bottom',
+          center: 'center',
+          left: 'left',
+          right: 'right',
+          message: 'top',
+          dialog: 'center',
+          share: 'bottom',
+        },
+        maskClass: {
+          position: 'fixed',
+          bottom: 0,
+          top: 0,
+          left: 0,
+          right: 0,
+          backgroundColor: 'rgba(0, 0, 0, 0.4)',
+        },
+        transClass: {
+          position: 'fixed',
+          left: 0,
+          right: 0,
+        },
+        maskShow: true,
+        mkclick: true,
+        popupstyle: this.isDesktop ? 'fixforpc-top' : 'top',
+      };
+    },
+    computed: {
+      isDesktop() {
+        return this.popupWidth >= 500 && this.popupHeight >= 500;
+      },
+      bg() {
+        if (this.backgroundColor === '' || this.backgroundColor === 'none') {
+          return 'transparent';
+        }
+        return this.backgroundColor;
+      },
+      borderRadius() {
+        if (this.round) {
+          if (this.type === 'bottom') {
+            return {
+              'border-top-left-radius': parseFloat(this.round) + 'px',
+              'border-top-right-radius': parseFloat(this.round) + 'px',
+            };
+          }
+          if (this.type === 'center') {
+            return {
+              'border-top-left-radius': parseFloat(this.round) + 'px',
+              'border-top-right-radius': parseFloat(this.round) + 'px',
+              'border-bottom-left-radius': parseFloat(this.round) + 'px',
+              'border-bottom-right-radius': parseFloat(this.round) + 'px',
+            };
+          }
+          if (this.type === 'top') {
+            return {
+              'border-bottom-left-radius': parseFloat(this.round) + 'px',
+              'border-bottom-right-radius': parseFloat(this.round) + 'px',
+            };
+          }
+        }
+      },
+    },
+    mounted() {
+      const fixSize = () => {
+        const { windowWidth, windowHeight, windowTop, safeArea, screenHeight, safeAreaInsets } = uni.getSystemInfoSync() || {} // sheep.$platform.device;
+        this.popupWidth = windowWidth;
+        this.popupHeight = windowHeight + (windowTop || 0);
+        // TODO fix by mehaotian 是否适配底部安全区 ,目前微信ios 、和 app ios 计算有差异,需要框架修复
+        if (safeArea && this.safeArea) {
+          // #ifdef MP-WEIXIN
+          this.safeAreaInsets = screenHeight - safeArea.bottom;
+          // #endif
+          // #ifndef MP-WEIXIN
+          this.safeAreaInsets = safeAreaInsets.bottom;
+          // #endif
+        } else {
+          this.safeAreaInsets = 0;
+        }
+      };
+      fixSize();
+      // #ifdef H5
+      // window.addEventListener('resize', fixSize)
+      // this.$once('hook:beforeDestroy', () => {
+      // 	window.removeEventListener('resize', fixSize)
+      // })
+      // #endif
+    },
+    // #ifndef VUE3
+    // TODO vue2
+    destroyed() {
+      this.setH5Visible();
+    },
+    // #endif
+    // #ifdef VUE3
+    // TODO vue3
+    unmounted() {
+      this.setH5Visible();
+    },
+    // #endif
+    created() {
+      // this.mkclick =  this.isMaskClick || this.maskClick
+      if (this.isMaskClick === null && this.maskClick === null) {
+        this.mkclick = true;
+      } else {
+        this.mkclick = this.isMaskClick !== null ? this.isMaskClick : this.maskClick;
+      }
+      if (this.animation) {
+        this.duration = 300;
+      } else {
+        this.duration = 0;
+      }
+      // TODO 处理 message 组件生命周期异常的问题
+      this.messageChild = null;
+      // TODO 解决头条冒泡的问题
+      this.clearPropagation = false;
+      this.maskClass.backgroundColor = this.maskBackgroundColor;
+    },
+    methods: {
+      setH5Visible() {
+        // #ifdef H5
+        // fix by mehaotian 处理 h5 滚动穿透的问题
+        document.getElementsByTagName('body')[0].style.overflow = 'visible';
+        // #endif
+      },
+      /**
+       * 公用方法,不显示遮罩层
+       */
+      closeMask() {
+        this.maskShow = false;
+      },
+      /**
+       * 公用方法,遮罩层禁止点击
+       */
+      disableMask() {
+        this.mkclick = false;
+      },
+      // TODO nvue 取消冒泡
+      clear(e) {
+        // #ifndef APP-NVUE
+        e.stopPropagation();
+        // #endif
+        this.clearPropagation = true;
+      },
+
+      open(direction) {
+        // fix by mehaotian 处理快速打开关闭的情况
+        if (this.showPopup) {
+          clearTimeout(this.timer);
+          this.showPopup = false;
+        }
+        let innerType = ['top', 'center', 'bottom', 'left', 'right', 'message', 'dialog', 'share'];
+        if (!(direction && innerType.indexOf(direction) !== -1)) {
+          direction = this.type;
+        }
+        if (!this.config[direction]) {
+          console.error('缺少类型:', direction);
+          return;
+        }
+        this[this.config[direction]]();
+        this.$emit('change', {
+          show: true,
+          type: direction,
+        });
+      },
+      close(type) {
+        this.showTrans = false;
+        this.$emit('change', {
+          show: false,
+          type: this.type,
+        });
+        this.$emit('close');
+        clearTimeout(this.timer);
+        // // 自定义关闭事件
+        // this.customOpen && this.customClose()
+        this.timer = setTimeout(() => {
+          this.showPopup = false;
+        }, 300);
+      },
+      // TODO 处理冒泡事件,头条的冒泡事件有问题 ,先这样兼容
+      touchstart() {
+        this.clearPropagation = false;
+      },
+
+      onTap() {
+        if (this.clearPropagation) {
+          // fix by mehaotian 兼容 nvue
+          this.clearPropagation = false;
+          return;
+        }
+        this.$emit('maskClick');
+        if (!this.mkclick) return;
+        this.close();
+      },
+      /**
+       * 顶部弹出样式处理
+       */
+      top(type) {
+        this.popupstyle = this.isDesktop ? 'fixforpc-top' : 'top';
+        this.ani = ['slide-top'];
+        this.transClass = {
+          position: 'fixed',
+          left: 0,
+          right: 0,
+          top: this.space + 'px',
+          backgroundColor: this.bg,
+        };
+        // TODO 兼容 type 属性 ,后续会废弃
+        if (type) return;
+        this.showPopup = true;
+        this.showTrans = true;
+        this.$nextTick(() => {
+          if (this.messageChild && this.type === 'message') {
+            this.messageChild.timerClose();
+          }
+        });
+      },
+      /**
+       * 底部弹出样式处理
+       */
+      bottom(type) {
+        this.popupstyle = 'bottom';
+        this.ani = ['slide-bottom'];
+        this.transClass = {
+          position: 'fixed',
+          left: 0,
+          right: 0,
+          bottom: 0,
+          paddingBottom: this.safeAreaInsets + this.space + 'px',
+          backgroundColor: this.bg,
+        };
+        // TODO 兼容 type 属性 ,后续会废弃
+        if (type) return;
+        this.showPopup = true;
+        this.showTrans = true;
+      },
+      /**
+       * 中间弹出样式处理
+       */
+      center(type) {
+        this.popupstyle = 'center';
+        this.ani = ['zoom-out', 'fade'];
+        this.transClass = {
+          position: 'fixed',
+          /* #ifndef APP-NVUE */
+          display: 'flex',
+          flexDirection: 'column',
+          /* #endif */
+          bottom: 0,
+          left: 0,
+          right: 0,
+          top: 0,
+          justifyContent: 'center',
+          alignItems: 'center',
+        };
+        // TODO 兼容 type 属性 ,后续会废弃
+        if (type) return;
+        this.showPopup = true;
+        this.showTrans = true;
+      },
+      left(type) {
+        this.popupstyle = 'left';
+        this.ani = ['slide-left'];
+        this.transClass = {
+          position: 'fixed',
+          left: 0,
+          bottom: 0,
+          top: 0,
+          backgroundColor: this.bg,
+          /* #ifndef APP-NVUE */
+          display: 'flex',
+          flexDirection: 'column',
+          /* #endif */
+        };
+        // TODO 兼容 type 属性 ,后续会废弃
+        if (type) return;
+        this.showPopup = true;
+        this.showTrans = true;
+      },
+      right(type) {
+        this.popupstyle = 'right';
+        this.ani = ['slide-right'];
+        this.transClass = {
+          position: 'fixed',
+          bottom: 0,
+          right: 0,
+          top: 0,
+          backgroundColor: this.bg,
+          /* #ifndef APP-NVUE */
+          display: 'flex',
+          flexDirection: 'column',
+          /* #endif */
+        };
+        // TODO 兼容 type 属性 ,后续会废弃
+        if (type) return;
+        this.showPopup = true;
+        this.showTrans = true;
+      },
+    },
+  };
+</script>
+<style lang="scss">
+  // 关闭icon
+  .close-icon {
+    position: absolute;
+    left: 50%;
+    transform: translateX(-50%);
+    bottom: -80rpx;
+    z-index: 100;
+  }
+
+  .uni-popup {
+    position: fixed;
+    /* #ifndef APP-NVUE */
+    z-index: 99;
+
+    /* #endif */
+    &.top,
+    &.left,
+    &.right {
+      /* #ifdef H5 */
+      top: var(--window-top);
+      /* #endif */
+      /* #ifndef H5 */
+      top: 0;
+      /* #endif */
+    }
+
+    .uni-popup__wrapper {
+      /* #ifndef APP-NVUE */
+      display: block;
+      /* #endif */
+      position: relative;
+      background: v-bind(backgroundImage) no-repeat;
+      background-size: 100% 100%;
+
+      /* iphonex 等安全区设置,底部安全区适配 */
+      /* #ifndef APP-NVUE */
+      // padding-bottom: constant(safe-area-inset-bottom);
+      // padding-bottom: env(safe-area-inset-bottom);
+      /* #endif */
+      &.left,
+      &.right {
+        /* #ifdef H5 */
+        padding-top: var(--window-top);
+        /* #endif */
+        /* #ifndef H5 */
+        padding-top: 0;
+        /* #endif */
+        flex: 1;
+      }
+    }
+  }
+
+  .fixforpc-z-index {
+    /* #ifndef APP-NVUE */
+    z-index: 999;
+    /* #endif */
+  }
+
+  .fixforpc-top {
+    top: 0;
+  }
+</style>

+ 45 - 0
components/ui/ct-popup/keypress.js

@@ -0,0 +1,45 @@
+// #ifdef H5
+export default {
+  name: 'Keypress',
+  props: {
+    disable: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  mounted() {
+    const keyNames = {
+      esc: ['Esc', 'Escape'],
+      tab: 'Tab',
+      enter: 'Enter',
+      space: [' ', 'Spacebar'],
+      up: ['Up', 'ArrowUp'],
+      left: ['Left', 'ArrowLeft'],
+      right: ['Right', 'ArrowRight'],
+      down: ['Down', 'ArrowDown'],
+      delete: ['Backspace', 'Delete', 'Del'],
+    };
+    const listener = ($event) => {
+      if (this.disable) {
+        return;
+      }
+      const keyName = Object.keys(keyNames).find((key) => {
+        const keyName = $event.key;
+        const value = keyNames[key];
+        return value === keyName || (Array.isArray(value) && value.includes(keyName));
+      });
+      if (keyName) {
+        // 避免和其他按键事件冲突
+        setTimeout(() => {
+          this.$emit(keyName, {});
+        }, 0);
+      }
+    };
+    document.addEventListener('keyup', listener);
+    // this.$once('hook:beforeDestroy', () => {
+    //   document.removeEventListener('keyup', listener)
+    // })
+  },
+  render: () => {},
+};
+// #endif

+ 51 - 4
hooks/useDictionaries.js

@@ -9,11 +9,52 @@ import {
   getAreaTreeData,
   getPositionData
 } from '@/api/common'
+import { getSecondNodes } from '@/utils/dealData'
 
-const setDict = (type, val, cacheTime = 7200) => {
+// const setDict = (type, val, cacheTime = 7200) => {
+const setDict = (type, val) => {
+  if (!val) {
+    return
+  }
+  if (type === 'areaTreeData') {
+    const obj = val.data.find(e => e.name === '中国')
+    val.data = 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
+    }) : []
+  }
+  if (type === 'areaTreeDataExtend') { // 前排加上不限
+    const obj = val.data.find(e => e.name === '中国')
+    if (obj?.children?.length) {
+      // const province = 
+      obj.children.forEach(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
+        // 不限
+        extendFun(e)
+      })
+      val.data = obj.children
+    } else val.data = []
+
+    function extendFun (e) {
+      const idType = Number.isInteger(e.id) ? 'int' : 'str'
+      if(e.children?.length) {
+        e.children.unshift({ name: '不限', id: e.id+'unlimited', idType })
+        e.children.forEach(i => extendFun(i))
+      }
+    }
+  }
+  // 一小时过期
+  const currentTime = new Date()
+  currentTime.setTime(currentTime.getTime() + 3600 * 1000)
+  
   uni.setStorageSync(type, JSON.stringify({
     data: val,
-    expire: Date.now() + cacheTime * 1000
+    // expire: Date.now() + cacheTime * 1000
+    expire: currentTime.getTime()
   }))
 }
 
@@ -24,7 +65,7 @@ export const getDict = (type, params, apiType = 'dict') => {
     return new Promise((resolve) => {
       const item = uni.getStorageSync(type)
       const catchData = item ? JSON.parse(item) : null
-      if (catchData && catchData.expire && (Date.now() <= catchData.expire)) {
+      if (catchData && catchData.expire && (Date.now() <= catchData.expire) && catchData.data && catchData.data?.data?.length) {
         return resolve({ data: catchData.data })
       }
       // 传参按照规范参数传
@@ -33,15 +74,21 @@ export const getDict = (type, params, apiType = 'dict') => {
         dict: getDictData,
         positionTreeData: getPositionTreeData, // 职位tree
         areaTreeData: getAreaTreeData, // 区域tree
+        areaTreeDataExtend: getAreaTreeData, // 区域tree(二级以后含不限)
         industryTreeData: getIndustryTreeData, // 行业tree
         industryList: getIndustryListData,
         skillList: getSkillList,
         areaList: getAreaListData,
         areaMap: getAreaMapData,
+        positionSecondData: getPositionTreeData,
         positionData: getPositionData
       }
       apiFn[apiType](query).then(data => {
-        setDict(type, data, Date.now())
+        // setDict(type, data, Date.now())
+        if (type === 'positionSecondData') {
+          data.data = getSecondNodes(data.data)
+        }
+        setDict(type, data)
         resolve({ data })
       })
     })

+ 432 - 0
hooks/useIM.js

@@ -0,0 +1,432 @@
+
+
+
+import { ref, onMounted, onUnmounted, watch } from 'vue';
+import { getConversationSync, getMessageSync, getChatKey, setUnread, deleteConversation } from '@/api/common'
+import { Base64 } from 'js-base64'
+
+import { userStore } from '@/store/user'
+import { useIMStore } from '@/store/im'
+
+
+// 配置悟空IM
+import {
+  MessageText,
+  Channel,
+  WKSDK,
+  ChannelTypePerson,
+  MessageContent,
+} from "wukongimjssdk"
+
+// 默认招呼语
+export const defaultText = '您好,关注到您发布该职位信息,请问有机会与您进一步沟通吗?'
+
+// 企业默认招呼语
+// export const defaultTextEnt = '您好,我们正在寻找充满激情、勇于挑战的您,快来和我聊一聊吧~'
+
+
+const { ObjectContent } = initRegister(101)
+const { ObjectContent: ObjectContent2 } = initRegister(102)
+const { ObjectContent: ObjectContent3 } = initRegister(103)
+const { ObjectContent: ObjectContent4 } = initRegister(104)
+const { ObjectContent: ObjectContent5 } = initRegister(105) // 发送简历
+
+
+const contentType = {
+  101: ObjectContent,
+  102: ObjectContent2,
+  103: ObjectContent3,
+  104: ObjectContent4,
+  105: ObjectContent5, // 发送简历
+}
+
+// 注册消息体
+function initRegister (type) {
+  class ObjectContent extends MessageContent {
+    constructor(text) {
+      super();
+      this.content = text
+    }
+    get conversationDigest() {
+        // 这里需要实现具体的逻辑
+        return this.content
+    }
+    get contentType() {
+        // 这里需要实现具体的逻辑
+        return type; // 示例实现
+    }
+    decodeJSON(content) {
+        this.content = content.text;
+    }
+    encodeJSON() {
+        return {
+          content: this.content
+        };
+    }
+  }
+  // 注册101类型为面试
+  WKSDK.shared().register(type, () => new ObjectContent(''))
+  return {
+    ObjectContent
+  }
+}
+
+
+const HISTORY_QUERY = {
+  limit: 20,
+  startMessageSeq: 0,
+  endMessageSeq: 0,
+  pullMode: 1
+}
+
+const ConnectStatus = {
+  Disconnect: 0, // 断开连接
+  Connected: 1, // 连接成功
+  Connecting: 2, // 连接中
+  ConnectFail: 3, // 连接错误
+  ConnectKick: 4, // 连接被踢,服务器要求客户端断开(一般是账号在其他地方登录,被踢)
+}
+// api 接入
+export function useDataSource () {
+  // 最近会话数据源
+  WKSDK.shared().config.provider.syncConversationsCallback  = async () => {
+    const query = {
+      msg_count: 1
+    }
+
+    const resultConversations = []
+    const resp = await getConversationSync(query)
+    const { data:conversationList } = resp
+    if (conversationList) {
+      conversationList.forEach(conversation => {
+        conversation.channel = new Channel(conversation.channel_id, conversation.channel_type)
+        conversation.unread = +(conversation.unread || 0)
+        resultConversations.push(conversation)
+      })
+    }
+    return resultConversations
+  }
+    // 同步频道消息数据源
+  WKSDK.shared().config.provider.syncMessagesCallback = async function(channel) {
+    // 后端提供的获取频道消息列表的接口数据 然后构建成 Message对象数组返回
+    let resultMessages  = new Array()
+    const {
+      startMessageSeq: start_message_seq,
+      endMessageSeq: end_message_seq,
+      limit,
+      pullMode: pull_mode
+    } = HISTORY_QUERY
+    const query = {
+      channel_id: channel.channelID,
+      channel_type: channel.channelType,
+      start_message_seq,
+      end_message_seq,
+      limit,
+      pull_mode,
+    }
+
+    const { data } = await getMessageSync(query)
+    const resp = data
+    const messageList = resp && resp["messages"]
+    if (messageList) {
+      messageList.forEach((msg) => {
+        // const message = Convert.toMessage(msg);
+        // msg.channel = new Channel(msg.channel_id, msg.channel_type)
+        msg.payload = JSON.parse(Base64.decode(msg.payload))
+        if (contentType[msg.payload.type]) {
+          msg.payload.content = JSON.parse(msg.payload.content ?? '{}')
+        }
+        resultMessages.push(msg)
+      })
+    }
+    // console.log(resultMessages)
+    const more = resp.more === 1
+    return {
+      more,
+      resultMessages
+    }
+  }
+}
+
+export function toChannel (channelID, channelType) {
+  return new Channel(channelID, channelType)
+}
+
+async function getKey () {
+  const useUserStore = userStore()
+  if (!useUserStore.accountInfo?.userId) {
+    return {}
+  }
+  const keyQuery = {
+    userId: useUserStore.accountInfo?.userId
+  }
+
+  const { data } = await getChatKey(keyQuery)
+  return {
+    ...data
+  }
+}
+
+export const useIM = () => {
+  useDataSource()
+  const key = ref(0)
+  const IM = useIMStore()
+  
+  onMounted( async () => {
+    await resetConfig()
+    // 连接状态监听
+    WKSDK.shared().connectManager.addConnectStatusListener(connectStatusListener)
+    // 常规消息监听
+    WKSDK.shared().chatManager.addMessageListener(messageListen)
+    // 连接
+    WKSDK.shared().connectManager.connect()
+  })
+  onUnmounted(() => {
+    WKSDK.shared().connectManager.removeConnectStatusListener(connectStatusListener)
+    // 常规消息监听移除
+    WKSDK.shared().chatManager.removeMessageListener(messageListen)
+    // 连接状态监听移除
+    WKSDK.shared().connectManager.disconnect()
+  })
+  
+  async function messageListen (message) {
+    // console.log('收到消息', message)
+    IM.setFromChannel(message.channel.channelID)
+    setUnreadCount()
+  }
+
+  async function connectStatusListener (status) {
+    // console.log('连接状态', status === ConnectStatus.Connected)
+    // 连接成功 获取点击数
+    const connected = status === ConnectStatus.Connected
+    IM.setConnected(connected)
+    if (connected) {
+      // 必须同步最近会话才能获取未读总数
+      await syncConversation()
+      setUnreadCount()
+    }
+  }
+
+  function setUnreadCount () {
+    const count = WKSDK.shared().conversationManager.getAllUnreadCount()
+    key.value++
+    IM.setNewMsg(key.value)
+    IM.setUnreadCount(count)
+    console.log('未读消息总数', count)
+  }
+
+  async function resetConfig () {
+    try {
+      const { uid, wssUrl, token } = await getKey()
+      IM.setUid(uid)
+      // 单机模式可以直接设置地址
+      WKSDK.shared().config.addr = 'wss://' + wssUrl// 默认端口为5200 + wsUrl 
+      // 认证信息
+      WKSDK.shared().config.uid = uid // 用户uid(需要在悟空通讯端注册过)
+      WKSDK.shared().config.token = token // 用户token (需要在悟空通讯端注册过)
+    } catch (error) {
+      console.log(error)
+    }
+  }
+
+  return {
+    resetConfig
+  }
+}
+
+export function initConnect (callback = () => {}, mounted = () => {}) {
+  useDataSource()
+  const IM = useIMStore()
+  const conversationList = ref([])
+  const messageItems = ref([])
+
+  watch(
+    () => IM.newMsg,
+    async () => {
+      // 未读消息变化
+      updateConversation()
+      // 拉取最新消息 查看是否是自己的数据
+    },
+    {
+      deep: true,
+      immediate: true
+    }
+  )
+  onMounted(async () => {
+    // 消息发送状态监听
+    WKSDK.shared().chatManager.addMessageStatusListener(statusListen)
+    // 常规消息监听
+    // WKSDK.shared().chatManager.addMessageListener(messageListen)
+
+    mounted()
+  })
+  onUnmounted(() => {
+    // 消息发送状态监听移除
+    WKSDK.shared().chatManager.removeMessageStatusListener(statusListen)
+    // 常规消息监听移除
+    // WKSDK.shared().chatManager.removeMessageListener(messageListen)
+  })
+
+  // 消息发送状态监听
+  function statusListen (packet) {
+    console.log('发送状态', packet)
+    if (packet.reasonCode === 1) {
+      // 发送成功
+      console.log('发送成功')
+      // 添加一组成功数据
+      callback(true)
+    } else {
+      // 发送失败
+      console.log('发送失败')
+      // 添加一组失败数据
+      callback(false)
+    }
+  }
+
+  async function updateConversation () {
+    const res = await syncConversation()
+    conversationList.value = res
+  }
+
+  function updateUnreadCount () {
+    const count = WKSDK.shared().conversationManager.getAllUnreadCount()
+    IM.setUnreadCount(count)
+  } 
+
+  async function deleteConversations (channel, enterpriseId) {
+    const query = {
+      channel_id: channel.channelID,
+      channel_type: channel.channelType,
+      enterpriseId
+    }
+    await deleteConversation(query)
+  }
+
+  async function resetUnread (channel, enterpriseId) {
+    const query = {
+      channel_id: channel.channelID,
+      channel_type: channel.channelType,
+      enterpriseId,
+      unread: 0
+    }
+    const res = await setUnread(query)
+    return res
+  }
+
+  return {
+    resetUnread,
+    deleteConversations,
+    updateConversation,
+    updateUnreadCount,
+    conversationList,
+    messageItems,
+    // channel
+  }
+}
+
+// 同步最近会话
+async function syncConversation () {
+  const res = await WKSDK.shared().conversationManager.sync()
+  return res
+}
+
+// 发起聊天
+export async function initChart (userId, enterpriseId) {
+  try {
+    const channel = ref()
+    // const list = ref([])
+    const query = {
+      userId,
+      enterpriseId
+    }
+    // 创建聊天频道
+    const { data } = await getChatKey(query)
+    // console.log(data, 'data')
+    const { uid } = data
+    const _channel = new Channel(uid, ChannelTypePerson)
+    channel.value = _channel
+    const conversation = WKSDK.shared().conversationManager.findConversation(_channel)
+    if(!conversation) {
+      // 如果最近会话不存在,则创建一个空的会话
+      WKSDK.shared().conversationManager.createEmptyConversation(_channel)
+    }
+    const res = await getMoreMessages(1, _channel)
+    return {
+      channel,
+      ...res
+    }
+  } catch (error) {
+    console.log(error)
+  }
+}
+
+// 翻页
+export async function getMoreMessages (pageSize, channel) {
+  const list = ref([])
+  Object.assign(HISTORY_QUERY, {
+    startMessageSeq: (pageSize - 1) * HISTORY_QUERY.limit
+  })
+  const { resultMessages, more } = await WKSDK.shared().chatManager.syncMessages(channel)
+  list.value = resultMessages
+  return {
+    list,
+    more
+  }
+}
+
+/**
+ * 
+ * @param {*} text 
+ * @param {*} _channel 
+ * @param { Number } type : 101 面试主体 
+ * @returns 
+ */
+  // 发送职位使用101
+export function send (text, _channel, type) {
+  let _text
+  if (contentType[type]) {
+    _text = new contentType[type](text)
+    WKSDK.shared().chatManager.send(_text, _channel)
+    return
+  }
+  _text = new MessageText(text)
+  console.log(WKSDK.shared().chatManager, 111111)
+  WKSDK.shared().chatManager.send(_text, _channel)
+}
+
+// 对话开场白 用户 to 企业
+export async function prologue ({userId, enterpriseId, text}) {
+  const { channel } = await checkConversation(userId, enterpriseId)
+  send(text, channel, 102)
+  return channel
+}
+
+// 企业 to 用户
+export async function talkToUser ({userId, text}) {
+  const { channel, isNewTalk } = await checkConversation(userId)
+  if (!isNewTalk) send(text, channel)
+}
+
+// 检测是否存在频道
+export async function checkConversation (userId, enterpriseId) {
+  const query = {
+    userId,
+    enterpriseId
+  }
+  // 创建聊天频道
+  const { data } = await getChatKey(query)
+  const { uid } = data
+  const _channel = new Channel(uid, ChannelTypePerson)
+  console.log('生成channel', _channel)
+  const conversation = WKSDK.shared().conversationManager.findConversation(_channel)
+  const isNewTalk = ref(false)
+  if(!conversation) {
+    // 如果最近会话不存在,则创建一个空的会话
+    WKSDK.shared().conversationManager.createEmptyConversation(_channel)
+    isNewTalk.value = true
+  }
+  return {
+    channel: _channel,
+    isNewTalk: isNewTalk.value
+  }
+}

+ 77 - 0
hooks/useModal.js

@@ -0,0 +1,77 @@
+import { modalStore } from '@/store/modal';
+const modal = modalStore()
+
+// 隐藏TabBar
+const hideBar = () => {
+  // 关闭tabBar
+  const currentPage = getCurrentPages()
+  if (!currentPage) {
+    return
+  }
+  const currentTabBar = currentPage[0]?.getTabBar?.();
+  // console.log('打开了', currentTabBar)
+  currentTabBar?.setData({ show: false });
+  // uni.hideTabBar({
+  //   success: () => {},
+  //   fail: () => {} // 捕获报错,防止没有tabbar页面调用后控制台报错
+  // }) 
+}
+// 显示TabBar
+const showBar = () => {
+  
+  // 打开tabBar
+  const currentPage = getCurrentPages()
+  if (!currentPage) {
+    return
+  }
+  const currentTabBar = currentPage[0]?.getTabBar?.();
+  // console.log('打开了', currentTabBar)
+  currentTabBar?.setData({ show: true });
+  // uni.showTabBar({
+  //   success: () => {},
+  //   fail: () => {} // 捕获报错,防止没有tabbar页面调用后控制台报错
+  // }) 
+}
+
+// 打开授权弹框
+export function showAuthModal(type = 'login') {
+  if (modal.auth !== '') {
+    // 注意:延迟修改,保证下面的 closeAuthModal 先执行掉
+    setTimeout(() => {
+      hideBar()
+      modal.$patch((state) => {
+        state.auth = type
+      })
+    }, 500)
+    closeAuthModal()
+  } else {
+    hideBar()
+    modal.$patch((state) => {
+      state.auth = type
+    })
+  }
+}
+
+// 关闭授权弹框
+export function closeAuthModal() {
+  showBar()
+  modal.$patch((state) => {
+    state.auth = ''
+  })
+}
+
+// 打开分享弹框
+export function showShareModal() {
+  hideBar()
+  modal.$patch((state) => {
+    state.share = true
+  })
+}
+
+// 关闭分享弹框
+export function closeShareModal() {
+  showBar()
+  modal.$patch((state) => {
+    state.share = false
+  })
+}

+ 35 - 0
layout/components/auth-modal.vue

@@ -0,0 +1,35 @@
+<!--  -->
+<template>
+  <view v-if="authType !== ''">
+    <ct-popup :show="authType !== 'necessaryInfo'" round="10" :showClose="true" @close="closeAuthModal">
+      <login></login>
+    </ct-popup>
+    <ct-popup :show="authType === 'necessaryInfo'" round="0" :showClose="false" @close="closeAuthModal">
+      <necessaryInfo></necessaryInfo>
+    </ct-popup>
+    <ct-popup :show="authType === 'selectUserType'" round="0" :showClose="false" @close="closeAuthModal">
+      <selectUserType></selectUserType>
+    </ct-popup>
+  </view>
+</template>
+
+<script setup>
+import { closeAuthModal } from '@/hooks/useModal'
+import ctPopup from '@/components/ui/ct-popup'
+import { modalStore } from '@/store/modal';
+import login from './authModal/login'
+import necessaryInfo from './authModal/necessaryInfo'
+import selectUserType from './authModal/selectUserType'
+
+
+const modal = modalStore()
+import { computed } from 'vue'
+
+
+// 授权弹窗类型
+const authType = computed(() => modal.auth)
+
+
+</script>
+<style lang="scss" scoped>
+</style>

+ 338 - 0
layout/components/authModal/login/index.vue

@@ -0,0 +1,338 @@
+<template>
+  <view class="ss-p-30 head-box">
+    <template v-if="changeType === 'login'">
+      <view class="head-title ss-m-b-30">欢迎来到门墩儿</view>
+      <uni-segmented-control
+        class="ss-m-t-30"
+        :current="current"
+        :values="items"
+        style-type="text" 
+        active-color="#00B760" 
+        @clickItem="onClickItem"
+      />
+      <!-- <view class="head-subtitle ss-m-t-10">未注册的手机号,验证后自动注册账号</view> -->
+
+      <view class="ss-m-t-30">
+        <!-- 手机号快捷登录 -->
+        <view v-if="current === 0">
+          <button v-if="!protocol" class="send-button" @click="showProtocolToast">手机号快捷登录</button>
+          <button v-else class="send-button" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">手机号快捷登录</button>
+        </view>
+
+        <!-- 短信验证码登录 -->
+        <uni-forms
+          v-if="current === 1"
+          ref="smsLoginRef"
+          v-model="state.sms"
+          :rules="state.smsRules"
+          validateTrigger="bind"
+          labelWidth="140"
+          labelAlign="center"
+        >
+          <uni-forms-item name="phone" label="手机号">
+            <uni-easyinput placeholder="请输入手机号" v-model="state.sms.phone" :inputBorder="false" type="number">
+              
+            </uni-easyinput>
+          </uni-forms-item>
+
+          <uni-forms-item name="code" label="验证码">
+            <uni-easyinput placeholder="请输入验证码" v-model="state.sms.code" :inputBorder="false" type="number" maxlength="6">
+              <template v-slot:right>
+                <button class="login-code" :disabled="state.isMobileEnd" :class="{ 'code-btn-end': state.isMobileEnd }" @tap="handleCode">
+                  {{ getSmsTimer('smsLogin') }}
+                </button>
+              </template>
+            </uni-easyinput>
+          </uni-forms-item>
+        </uni-forms>
+
+        <!-- 账号密码登录 -->
+        <uni-forms
+          v-if="current === 2"
+          ref="accountLoginRef"
+          v-model="state.account"
+          :rules="state.rules"
+          validateTrigger="bind"
+          labelWidth="140"
+          labelAlign="center"
+        >
+          <uni-forms-item name="phone" label="账号">
+            <uni-easyinput placeholder="请输入账号" v-model="state.account.phone" :inputBorder="false"></uni-easyinput>
+          </uni-forms-item>
+
+          <uni-forms-item name="password" label="密码">
+            <uni-easyinput type="password" placeholder="请输入密码" v-model="state.account.password" :inputBorder="false"></uni-easyinput>
+          </uni-forms-item>
+        </uni-forms>
+
+        <view class="quickLogon">
+          <view>
+            <!-- <button v-if="!protocol" class="wxLogon" type="text" :plain="true" @click="showProtocolToast">微信一键登录</button>
+            <button v-else class="wxLogon" type="text" :plain="true" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">微信一键登录</button> -->
+          </view>
+          <view class="register" @tap="handleChangeRegister">还没有登录账户?去注册</view>
+        </view>
+        <button v-if="current !== 0" class="send-button" @tap="handleLogin"> 登 录  </button>
+        <view class="agreement-box ss-flex ss-row-center" style="margin-bottom: 30px;">
+          <uni-icons size="20" :type="protocol ? 'checkbox-filled' : 'circle'" :color="protocol ? '#00B760' : '#ccc'" @tap="protocol = !protocol"></uni-icons>
+          <view class="color-999 ss-flex ss-col-center ss-m-l-8 font-size-13">
+            我已阅读并遵守
+            <view class="color-primary" @tap.stop="handleToDetail('user')">
+              《用户协议》
+            </view>
+            <view class="agreement-text">和</view>
+            <view class="color-primary" @tap.stop="handleToDetail('privacy')">
+              《隐私协议》
+            </view>
+          </view>
+        </view>
+      </view>
+    </template>
+    <template v-if="changeType === 'register'">
+      <view class="head-title pb-20">手机号注册</view>
+
+      <uni-forms
+        ref="registerForm"
+        v-model="state.register"
+        :rules="state.smsRules"
+        validateTrigger="bind"
+        labelWidth="140"
+        labelAlign="center"
+      >
+
+        <uni-forms-item name="phone" label="手机号">
+          <uni-easyinput placeholder="请输入手机号" v-model="state.register.phone" :inputBorder="false" type="number">
+          </uni-easyinput>
+        </uni-forms-item>
+
+        <uni-forms-item name="code" label="验证码">
+          <uni-easyinput
+            placeholder="请输入验证码"
+            v-model="state.register.code"
+            :inputBorder="false"
+            type="number"
+            maxlength="6"
+          >
+            <template v-slot:right>
+              <button
+                class="login-code"
+                :disabled="state.isMobileEnd"
+                :class="{ 'code-btn-end': state.isMobileEnd }"
+                @tap="handleRegisterCode"
+              >
+                {{ getSmsTimer('smsRegister') }}
+              </button>
+            </template>
+          </uni-easyinput>
+        </uni-forms-item>
+      </uni-forms>
+      <view class="register login" style="text-align: end;" @tap="handleChangeLogin">已有账户?去登陆</view>
+      <view>
+        <button class="send-button" @tap="handleRegister"> 注 册 </button>
+      </view>
+      <view class="color-999 ss-flex ss-col-center ss-row-center ss-m-l-8 font-size-13" style="margin-bottom: 30px;">
+        点击注册即代表您同意
+        <view class="color-primary" @tap.stop="handleToDetail('user')">
+          《用户协议》
+        </view>
+        <view class="agreement-text">和</view>
+        <view class="color-primary" @tap.stop="handleToDetail('privacy')">
+          《隐私协议》
+        </view>
+      </view>
+    </template>
+  </view>
+</template>
+
+<script setup>
+import { ref, unref } from 'vue'
+import { mobile, password, code } from '@/utils/validate'
+import { getSmsCode, getSmsTimer } from '@/utils/code'
+import { userStore } from '@/store/user'
+
+const useUserStore = userStore()
+const items = ['手机号快捷登录', '短信登录', '账号登录']
+const current = ref(0)
+const accountLoginRef = ref()
+const smsLoginRef = ref()
+const registerForm = ref()
+const protocol = ref(false)
+const state = ref({
+  isMobileEnd: false, // 手机号输入完毕
+  codeText: '获取验证码',
+  sms: {
+    phone: '',
+    code: ''
+  },
+  register: {
+    phone: '',
+    code: ''
+  },
+  account: {
+    phone: '',
+    password: ''
+  },
+  rules: {
+    phone: mobile,
+    password,
+  },
+  smsRules: {
+    code,
+    phone: mobile
+  }
+})
+
+
+// 设置默认账号密码便于开发快捷登录
+if (window && window.location && window.location.hostname && window.location.hostname === 'localhost') {
+  state.value.account.phone = '13229740092'
+  state.value.account.password = 'Citu123456'
+}
+
+const changeType = ref('login')
+
+// 登录成功后的返回页面
+// const backUrl = getCurrentPages().length ? getCurrentPages()[getCurrentPages().length - 2].route : ''
+
+const onClickItem = (e) => {
+  current.value = e.currentIndex
+}
+
+// 获取验证码
+const handleCode = () => {
+  if (!state.value.sms.phone) {
+    uni.showToast({
+      title: '请输入手机号',
+      icon: 'none',
+      duration: 2000
+    })
+    return
+  }
+  getSmsCode('smsLogin', state.value.sms.phone)
+}
+
+// 获取验证码
+const handleRegisterCode = () => {
+  if (!state.value.register.phone) {
+    uni.showToast({
+      title: '请输入手机号',
+      icon: 'none',
+      duration: 2000
+    })
+    return
+  }
+  getSmsCode('smsRegister', state.value.register.phone)
+}
+// 查看协议详情
+const handleToDetail = (type) => {
+  const url = type === 'user' ? '/pagesB/agreement/user' : '/pagesB/agreement/privacy'
+  uni.navigateTo({
+    url
+  })
+}
+
+// 注册
+function handleChangeRegister () {
+  changeType.value = 'register'
+}
+
+// 登录
+function handleChangeLogin () {
+  changeType.value = 'login'
+}
+
+const showProtocolToast = () => {
+  uni.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
+}
+
+// 微信登录
+const getPhoneNumber = async (e) => {
+  if (e?.detail?.errMsg !== 'getPhoneNumber:ok') {
+    uni.showToast({ title: '微信登录失败', icon: 'none' })
+    return
+  }
+  changeType.value = 'login'
+  wx.login({
+    success: async (result) => {
+      const wxLoginCode = result?.code || ''
+      const query = {
+        loginCode: wxLoginCode,
+        phoneCode: e.detail.code,
+        state: e.detail.encryptedData,
+      }
+      await useUserStore.handleSmsLogin(query, current.value)
+    },
+    fail:(res)=> { console.log("获取登录凭证code失败!", res) }
+  })
+}
+
+async function handleRegister () {
+  const validate = await unref(registerForm).validate()
+  if (!validate) return
+  try {
+    await useUserStore.handleRegister(state.value.register)
+    changeType.value = 'login'
+  } finally {
+
+  }
+}
+
+// 登录
+const handleLogin = async () => {
+  if (!protocol.value) return uni.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
+  const validate = await unref(current.value === 1 ? smsLoginRef : accountLoginRef).validate()
+  if (!validate) return
+  const query = current.value === 1 ? state.value.sms : state.value.account
+  Object.assign(query, {
+    account: query.phone
+  })
+  
+  await useUserStore.handleSmsLogin(query, current.value)
+}
+</script>
+
+<style scoped lang="scss">
+.login-code {
+  width: 73px;
+  min-width: 73px;
+  color: #00B760;
+  text-align: center; 
+  font-size: 12px; 
+  cursor: pointer;
+  // border: 1px dashed #00B760;
+  // border-radius: 26px;
+  padding: 0;
+}
+.head-title {
+  font-size: 40rpx;
+  text-align: center;
+  color: #00B760;
+  margin-top: 30rpx;
+}
+.quickLogon {
+  display: flex;
+  justify-content: space-between;
+  padding: 0 20rpx;
+  align-items: center;
+}
+.wxLogon {
+  font-size: .85em;
+  color: #00B760;
+  border: none;
+  margin: 0;
+  padding: 0;
+}
+.register {
+  widows: 100%;
+  font-size: .85em;
+  color: red;
+  // text-align: right;
+  &.login {
+    color: #00B760;
+  }
+}
+.pb-20 {
+  padding-bottom: 40rpx;
+}
+
+</style>

+ 248 - 0
layout/components/authModal/necessaryInfo/index.vue

@@ -0,0 +1,248 @@
+<!-- 校验是否完善人才必填信息 -->
+<template>
+  <scroll-view class="scrollBox" scroll-y="true">
+    <view class="content">
+      <view class="text-center ss-m-b-50 font-size-20 color-primary">请完善您的基本信息</view>
+      <uni-forms
+        ref="baseInfoRef"
+        v-model="formData"
+        :rules="formRules"
+        validateTrigger="bind"
+        label-width="86px"
+        labelAlign="center"
+      >
+        <uni-forms-item label="头像" name="avatar" class="f-straight" required>
+          <view style="display: flex;flex-wrap: wrap;">
+            <view class="upload-img" v-if="formData?.avatar">
+              <uni-icons size="35" type="clear" color="#fe574a" style="position: absolute;right: -15px; top: -15px; z-index: 9" @click="formData.avatar = ''"></uni-icons>
+              <image :src="formData?.avatar" mode="contain" style="width: 200rpx;height: 200rpx;" @click="handlePreviewImage"></image>
+            </view>
+            <view v-else class="upload-file" @click="uploadPhotos">
+              <uni-icons type="plusempty" size="50" color="#f1f1f1"></uni-icons>
+            </view>
+          </view>
+        </uni-forms-item>
+        <uni-forms-item name="name" label="姓名" required>
+          <uni-easyinput placeholder="请输入姓名" v-model="formData.name" :inputBorder="false" type="text"></uni-easyinput>
+        </uni-forms-item>
+        <uni-forms-item name="sex" label="性别" required>
+          <uni-data-picker v-model="formData.sex" :localdata="dictObj.sex" :clear-icon="false" popup-title="请选择性别" :map="{ text: 'label', value: 'value' }"></uni-data-picker>
+        </uni-forms-item>
+        <uni-forms-item name="phone" label="联系电话" required>
+          <uni-easyinput placeholder="请输入联系电话" v-model="formData.phone" :inputBorder="false" type="number"></uni-easyinput>
+        </uni-forms-item>
+        <uni-forms-item name="email" label="常用邮箱" required>
+          <uni-easyinput v-model="formData.email" placeholder="请输入常用邮箱" />
+        </uni-forms-item>
+        <uni-forms-item required label="出生日期" name="birthday">
+          <uni-datetime-picker type="date" return-type="timestamp" v-model="formData.birthday" />
+        </uni-forms-item>
+        <uni-forms-item name="enterpriseName" label="任职企业名称" required>
+          <uni-easyinput placeholder="请填写任职企业名称(可填暂无)" v-model="formData.enterpriseName" :clear-icon="true" type="text"></uni-easyinput>
+        </uni-forms-item>
+        <uni-forms-item name="positionName" label="任职职位名称" required>
+          <uni-easyinput placeholder="请填写任职职位名称(可填暂无)" v-model="formData.positionName" :clear-icon="true" type="text"></uni-easyinput>
+        </uni-forms-item>
+        <uni-forms-item name="interestedPositionId" label="意向职位" required>
+          <uni-data-picker v-model="formData.interestedPositionId" :localdata="dictObj.positionSecondData" :clear-icon="false" popup-title="请选择意向职位" :map="{ text: 'nameCn', value: 'id' }"></uni-data-picker>
+        </uni-forms-item>
+        <uni-forms-item name="jobStatus" label="求职状态" required>
+          <uni-data-picker v-model="formData.jobStatus" :localdata="dictObj.jobStatus" :clear-icon="false" popup-title="请选择求职状态" :map="{ text: 'label', value: 'value' }"></uni-data-picker>
+        </uni-forms-item>
+        <uni-forms-item name="expType" label="工作经验" required>
+          <uni-data-picker v-model="formData.expType" :localdata="dictObj.exp" :clear-icon="false" popup-title="请选择工作经验" :clear="false" :map="{ text: 'label', value: 'value' }"></uni-data-picker>
+        </uni-forms-item>
+        <uni-forms-item name="eduType" label="最高学历" required>
+          <uni-data-picker v-model="formData.eduType" :localdata="dictObj.edu" :clear-icon="false" popup-title="请选择最高学历" :clear="false" :map="{ text: 'label', value: 'value' }"></uni-data-picker>
+        </uni-forms-item>
+      </uni-forms>
+      <view class="f-horizon-center">
+        <button type="primary" size="default" class="send-button" @click="submit">提 交</button>
+      </view>
+      <view class="f-horizon-center">
+        <view class="changeRole" @tap="handleLogout">切换账号</view>
+      </view>
+    </view>
+    <uni-popup ref="logoutPopup" type="dialog">
+      <uni-popup-dialog type="warn" cancelText="取消" confirmText="确定" title="系统提示" content="确认退出账号?" @confirm="handleLogoutConfirm" @close="handleLogoutClose">
+      </uni-popup-dialog>
+    </uni-popup>
+  </scroll-view>
+</template>
+<script setup>
+import { ref, unref, onMounted, nextTick } from 'vue'
+import { mobile, emailRequired } from '@/utils/validate'
+import { dictObj } from '@/utils/position.js'
+import { savePersonSimpleInfo } from '@/api/user'
+import { showAuthModal } from '@/hooks/useModal'
+import { userStore } from '@/store/user'; const useUserStore = userStore()
+import { uploadFile } from '@/api/file'
+
+const baseInfoRef = ref()
+const formData = ref({ // 必填项目
+  name: null,
+  sex: null,
+  phone: null,
+  email: null,
+  birthday: '1990-01-01',
+  enterpriseName: null,
+  positionName: null,
+  interestedPositionId: null,
+  jobStatus: null,
+  expType: null,
+  eduType: null,
+})
+
+onMounted(() => {
+  nextTick(() => {
+    const baseInfo = useUserStore?.baseInfo || null
+    if (baseInfo && Object.keys(baseInfo).length) {
+      Object.keys(formData.value).forEach(key => {
+        formData.value[key] = baseInfo[key] || baseInfo[key] === 0 ? baseInfo[key] : formData.value[key] ? formData.value[key] : null
+      })
+    }
+    if (!formData.value.phone && useUserStore?.phone) formData.value.phone = useUserStore.phone
+  })
+})
+
+const formRules = {
+	avatar:{
+		rules: [{required: true, errorMessage: '请上传头像' }]
+	},
+	name:{
+		rules: [{required: true, errorMessage: '请输入姓名' }]
+	},
+  sex : {
+    rules: [{required: true, errorMessage: '请选择您的性别' }]
+  },
+  phone: mobile,
+  birthday:{
+		rules: [{required: true, errorMessage: '请选择您的出生日期' }]
+	},
+  enterpriseName:{
+		rules: [{required: true, errorMessage: '请填写任职企业名称(可填暂无)' }]
+	},
+  positionName:{
+		rules: [{required: true, errorMessage: '请填写任职职位名称(可填暂无)' }]
+	},
+  email: emailRequired,
+  expType: {
+		rules: [{required: true, errorMessage: '请选择您的工作年限' }]
+	},
+  eduType: {
+		rules: [{required: true, errorMessage: '请选择您的最高学历' }]
+	},
+  jobStatus: {
+		rules: [{required: true, errorMessage: '请选择您的求职状态' }]
+	}
+}
+
+const logoutPopup = ref()
+// 退出登录
+const handleLogout = () => {
+  logoutPopup.value.open()
+}
+const handleLogoutClose = () => {
+  logoutPopup.value.close()
+}
+const handleLogoutConfirm = async () => {
+  await useUserStore.handleLogout()
+  showAuthModal()
+}
+
+// 图片预览
+const handlePreviewImage = () => {
+  uni.previewImage({
+    current: 0,
+    urls: [formData.value.avatar]
+  })
+}
+// 选择头像
+const uploadPhotos = () => {
+  wx.chooseImage({
+    count: 1,
+    sizeType: ['original', 'compressed'],
+    sourceType: ['album', 'camera'],
+    success: function(res){
+      const size = res.tempFiles[0]?.size || 0
+      if (size >= 31457280) {
+        uni.showToast({
+          icon: 'none',
+          title: '头像上传大小不得超过 20MB !',
+          duration: 2000
+        })
+        return
+      }
+      const path = res.tempFilePaths[0]
+      uploadFile(path, 'img').then(res => {
+        formData.value.avatar = res.data
+      }).catch(error => {
+        uni.showToast({
+          icon: 'error',
+          title: '图片上传失败!',
+          duration: 2000
+        })
+      })
+    }
+  })
+}
+
+const dealQuery = () => {
+  const interestedList = [{ positionId: formData.value.interestedPositionId || null }]
+  const workExpList = [{
+    enterpriseName: formData.value.enterpriseName || null,
+    positionName: formData.value.positionName || null,
+  }]
+  return { ...formData.value, interestedList, workExpList }
+}
+const submit = async () => {
+  const validate = await unref(baseInfoRef).validate()
+  if (!validate) return uni.showToast({ title: '请将信息补充完整', icon: 'none' })
+  
+  try {
+    const query = dealQuery()
+    await savePersonSimpleInfo(query)
+    uni.showToast({ title: '保存成功', icon: 'none' })
+    await useUserStore.getInfo()
+    // await useUserStore.getUserInfo()
+  } catch (err) {
+    uni.showToast({ title: err?.msg || '保存失败', icon: 'none' })
+  }
+}
+
+</script>
+<style lang="scss" scoped>
+.scrollBox {
+  width: 100vw;
+  // height: 100vh;
+  height: calc(100vh - 30rpx);
+  margin-bottom: 30rpx;
+}
+.content {
+  padding: 30rpx;
+}
+
+.changeRole {
+  color: var(--color-666);
+  font-size: 15px;
+  line-height: 26px;
+  margin-bottom: 40rpx;
+}
+.upload-img{
+  position: relative;
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+}
+.upload-file{
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 10rpx;
+}
+</style>

+ 59 - 0
layout/components/authModal/selectUserType/index.vue

@@ -0,0 +1,59 @@
+<!-- 选择是否是学生 -->
+<template>
+  <scroll-view class="scrollBox" scroll-y="true">
+    <template v-if="showSelect">
+      <view class="text-center ss-m-t-50" style="color: #666; font-weight: 400; font-size: 18px">请选择当前角色</view>
+      <view class="ss-p-50">
+        <uni-card class="ss-m-t-30" @tap="handleClickRole(0)">
+          <view class="d-flex flex-column align-center ss-p-y-50">
+            <image src="/static/svg/workMan.svg" class="svg ss-m-l-10"></image>
+            <view class="color-primary text-center" style="font-size: 24px">职场人士</view>
+          </view>
+        </uni-card>
+        <uni-card class="ss-m-t-80" @tap="handleClickRole(1)">
+          <view class="d-flex flex-column align-center ss-p-y-50">
+            <image src="/static/svg/student.svg" class="svg ss-m-l-10"></image>
+            <view class="color-primary text-center" style="font-size: 24px">在校学生</view>
+          </view>
+        </uni-card>
+      </view>
+    </template>
+    <template v-else>
+      <studentInfoForm></studentInfoForm>
+    </template>
+  </scroll-view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import studentInfoForm from './studentInfoForm.vue'
+import { showAuthModal } from '@/hooks/useModal'
+
+const showSelect = ref(true)
+
+const handleClickRole = (type) => {
+  if (type) {
+    // 学生用户
+    showSelect.value = false
+  } else {
+    // 普通用户
+    showAuthModal('necessaryInfo')
+    uni.setStorageSync('necessaryInfoReady', 'fddeaddc47868b')
+  }
+}
+
+</script>
+<style lang="scss" scoped>
+.scrollBox {
+  width: 100vw;
+  // height: 100vh;
+  height: calc(100vh - 30rpx);
+  margin-bottom: 30rpx;
+}
+
+.svg {
+  width: 100px;
+  height: 100px;
+  margin-bottom: 30px;
+}
+</style>

+ 306 - 0
layout/components/authModal/selectUserType/studentInfoForm.vue

@@ -0,0 +1,306 @@
+<!-- 校验是否完善人才必填信息 -->
+<template>
+  <scroll-view class="scrollBox" scroll-y="true">
+    <view class="content">
+      <view class="text-center ss-m-b-50 font-size-20 color-primary">请完善学生信息</view>
+      <uni-forms
+        ref="baseInfoRef"
+        v-model="formData"
+        :rules="formRules"
+        validateTrigger="bind"
+        label-width="90px"
+        labelAlign="center"
+      >
+        <uni-forms-item label="头像" name="avatar" class="f-straight" required>
+          <view style="display: flex;flex-wrap: wrap;">
+            <view class="upload-img" v-if="formData?.avatar">
+              <uni-icons size="35" type="clear" color="#fe574a" style="position: absolute;right: -15px; top: -15px; z-index: 9" @click="formData.avatar = ''"></uni-icons>
+              <image :src="formData?.avatar" mode="contain" style="width: 200rpx;height: 200rpx;" @click="handlePreviewImage"></image>
+            </view>
+            <view v-else class="upload-file" @click="uploadPhotos">
+              <uni-icons type="plusempty" size="50" color="#f1f1f1"></uni-icons>
+            </view>
+          </view>
+        </uni-forms-item>
+        <uni-forms-item name="name" label="姓名" required>
+          <uni-easyinput placeholder="请输入姓名" v-model="formData.name" :inputBorder="false" type="text"></uni-easyinput>
+        </uni-forms-item>
+        <uni-forms-item name="sex" label="性别" required>
+          <uni-data-picker v-model="formData.sex" :localdata="dictObj.sex" :clear-icon="false" popup-title="请选择性别" :map="{ text: 'label', value: 'value' }"></uni-data-picker>
+        </uni-forms-item>
+        <uni-forms-item name="idCardNo" label="身份证号码" required>
+          <uni-easyinput placeholder="请输入身份证号码" v-model="formData.idCardNo" :inputBorder="false" type="text"></uni-easyinput>
+        </uni-forms-item>
+        <uni-forms-item name="phone" label="联系电话" required>
+          <uni-easyinput placeholder="请输入联系电话" v-model="formData.phone" :inputBorder="false" type="number"></uni-easyinput>
+        </uni-forms-item>
+        <uni-forms-item required label="出生日期" name="birthday">
+          <uni-datetime-picker type="date" return-type="timestamp" v-model="formData.birthday" />
+        </uni-forms-item>
+        <uni-forms-item name="schoolId" label="就读学校" required>
+          <uni-data-picker v-model="formData.schoolId" :localdata="selects?.schools" :clear-icon="false" popup-title="请选择就读学校" @change="getSelectData(0)" :map="{ text: 'name', value: 'schoolId' }"></uni-data-picker>
+        </uni-forms-item>
+        <uni-forms-item name="schoolDeptId" label="所在院系" required>
+          <uni-data-picker v-model="formData.schoolDeptId" :localdata="selects?.dept" :clear-icon="false" popup-title="请选择所在院系" @change="getSelectData(2)" :map="{ text: 'name', value: 'id' }"></uni-data-picker>
+        </uni-forms-item>
+        <uni-forms-item name="majorId" label="所学专业" required>
+          <uni-data-picker v-model="formData.majorId" :localdata="selects?.major" :clear-icon="false" popup-title="请选择所学专业" @change="getSelectData(2)" :map="{ text: 'nameCn', value: 'id' }"></uni-data-picker>
+        </uni-forms-item>
+        <uni-forms-item name="schoolClassId" label="所在班级" required>
+          <searchComBox ref="schoolClassIdRef" v-model="formData.schoolClassId" :candidates="classList" itemTextName='schoolClassName' itemValueName='schoolClassId' labelKey='name' valueKey='id' placeholder="请选择所在班级"></searchComBox>
+        </uni-forms-item>
+        <uni-forms-item name="studentNo" label="学号" required>
+          <uni-easyinput placeholder="请填写学号" v-model="formData.studentNo" :inputBorder="false" type="text"></uni-easyinput>
+        </uni-forms-item>
+        <uni-forms-item name="emergencyContactName" label="紧急联系人姓名" required>
+          <uni-easyinput placeholder="请填写紧急联系人姓名" v-model="formData.emergencyContactName" :inputBorder="false" type="text"></uni-easyinput>
+        </uni-forms-item>
+        <uni-forms-item name="emergencyContactPhone" label="紧急联系人手机号" required>
+          <uni-easyinput placeholder="请填写紧急联系人手机号" v-model="formData.emergencyContactPhone" :inputBorder="false" type="number"></uni-easyinput>
+        </uni-forms-item>
+      </uni-forms>
+      <view class="f-horizon-center">
+        <button type="primary" size="default" class="send-button" @click="submit">提 交</button>
+      </view>
+      <view class="f-horizon-center">
+        <view class="changeRole" @tap="handleLogout">退出登录</view>
+      </view>
+    </view>
+    <uni-popup ref="logoutPopup" type="dialog">
+      <uni-popup-dialog type="warn" cancelText="取消" confirmText="确定" title="系统提示" content="确认退出账号?" @confirm="handleLogoutConfirm" @close="handleLogoutClose">
+      </uni-popup-dialog>
+    </uni-popup>
+  </scroll-view>
+</template>
+<script setup>
+import { ref, unref, onMounted, nextTick } from 'vue'
+import { mobile, isValidIdCard18 } from '@/utils/validate'
+import { dictObj } from '@/utils/position.js'
+import { saveStudentSimpleInfo } from '@/api/user'
+import { showAuthModal } from '@/hooks/useModal'
+import { userStore } from '@/store/user'; const useUserStore = userStore()
+import { uploadFile } from '@/api/file'
+import { getSchoolList, getDepartmentListBySchoolId, getMajorList } from '@/api/student'
+import searchComBox from '@/components/searchCombox'
+
+const baseInfoRef = ref()
+const formData = ref({ // 必填项目
+  name: null,
+  sex: null,
+  idCardNo: null,
+  phone: null,
+  birthday: '1990-01-01',
+  schoolId: null,
+  schoolDeptId: null,
+  majorId: null,
+  schoolClassId: null,
+  schoolClassName: null,
+  studentNo: null,
+  idCardNo: null,
+  emergencyContactName: null,
+  emergencyContactPhone: null,
+})
+
+onMounted(() => {
+  nextTick(() => {
+    const baseInfo = useUserStore?.baseInfo || null
+    if (baseInfo && Object.keys(baseInfo).length) {
+      Object.keys(formData.value).forEach(key => {
+        formData.value[key] = baseInfo[key] || baseInfo[key] === 0 ? baseInfo[key] : formData.value[key] ? formData.value[key] : null
+      })
+    }
+    if (!formData.value.phone && useUserStore?.phone) formData.value.phone = useUserStore.phone
+  })
+})
+
+// // 下拉列表 
+const selects = ref({})
+const classList = ref([])
+const getSelectData = async (type = 'default', init = false) => { // type: 0院系|1专业|2班级
+  const params = { ...(type !== 'default' && { type }) }
+  // 查院系用 schoolId 查班级用 parentId
+  if (type === 0) {
+    if (!formData.value?.schoolId) return
+    params.schoolId = formData.value.schoolId
+  }
+  if (type === 2) {
+    if (!formData.value?.schoolId && !formData.value?.schoolDeptId) return
+    if (formData.value?.schoolId) params.schoolId = formData.value.schoolId
+    if (formData.value?.schoolDeptId) params.parentId = formData.value.schoolDeptId
+  }
+  const api = {
+    default: getSchoolList,
+    0: getDepartmentListBySchoolId,
+    1: getMajorList,
+    2: getDepartmentListBySchoolId,
+  }
+  const res = await api[type](params)
+  if (type === 'default') {
+    selects.value.schools = res?.data?.length ? res.data : []
+  }
+  if (type === 0) {
+    if (!init) {
+      formData.value.schoolDeptId = null
+      formData.value.schoolClassId = null
+    }
+    selects.value.dept = res?.data?.length ? res.data : []
+  }
+  if (type === 1) {
+    selects.value.major = res?.data?.length ? res.data : []
+  }
+  if (type === 2) {
+    if (!init) formData.value.schoolClassId = null
+    classList.value = res?.data?.length ? res.data : []
+    schoolClassIdRef.value && schoolClassIdRef.value.setLabel()
+  }
+}
+
+const getSelection = async () => {
+  await getSelectData('default', true)
+  await getSelectData(0, true)
+  await getSelectData(1, true)
+  await getSelectData(2, true)
+}
+getSelection()
+
+const formRules = {
+	avatar:{
+		rules: [{required: true, errorMessage: '请上传头像' }]
+	},
+	name:{
+		rules: [{required: true, errorMessage: '请输入姓名' }]
+	},
+  sex: {
+    rules: [{required: true, errorMessage: '请选择您的性别' }]
+  },
+  idCardNo: isValidIdCard18,
+  phone: mobile,
+  birthday:{
+		rules: [{required: true, errorMessage: '请选择出生日期' }]
+	},
+  schoolId:{
+		rules: [{required: true, errorMessage: '请选择就读学校' }]
+	},
+  schoolDeptId:{
+		rules: [{required: true, errorMessage: '请选择所在院系' }]
+	},
+  majorId: {
+		rules: [{required: true, errorMessage: '请填写所在专业' }]
+	},
+  schoolClassId: {
+		rules: [{required: true, errorMessage: '请填写所在班级' }]
+	},
+  studentNo: {
+		rules: [{required: true, errorMessage: '请填写学号' }]
+	},
+  emergencyContactName: {
+		rules: [{required: true, errorMessage: '请填写紧急联系人姓名' }]
+	},
+  emergencyContactPhone: mobile,
+}
+
+const logoutPopup = ref()
+// 退出登录
+const handleLogout = () => {
+  logoutPopup.value.open()
+}
+const handleLogoutClose = () => {
+  logoutPopup.value.close()
+}
+const handleLogoutConfirm = async () => {
+  await useUserStore.handleLogout()
+  showAuthModal()
+}
+
+// 图片预览
+const handlePreviewImage = () => {
+  uni.previewImage({
+    current: 0,
+    urls: [formData.value.avatar]
+  })
+}
+// 选择头像
+const uploadPhotos = () => {
+  wx.chooseImage({
+    count: 1,
+    sizeType: ['original', 'compressed'],
+    sourceType: ['album', 'camera'],
+    success: function(res){
+      const size = res.tempFiles[0]?.size || 0
+      if (size >= 31457280) {
+        uni.showToast({
+          icon: 'none',
+          title: '头像上传大小不得超过 20MB !',
+          duration: 2000
+        })
+        return
+      }
+      const path = res.tempFilePaths[0]
+      uploadFile(path, 'img').then(res => {
+        formData.value.avatar = res.data
+      }).catch(error => {
+        uni.showToast({
+          icon: 'error',
+          title: '图片上传失败!',
+          duration: 2000
+        })
+      })
+    }
+  })
+}
+
+const schoolClassIdRef = ref()
+const submit = async () => {
+  const validate = await unref(baseInfoRef).validate()
+  if (!validate) return uni.showToast({ title: '请将信息补充完整', icon: 'none' })
+  
+  let params = {...formData.value}
+
+  const schoolClassObj = schoolClassIdRef.value?.getValue()
+  params = {...params, ...schoolClassObj}
+
+  try {
+    await saveStudentSimpleInfo(params)
+    uni.showToast({ title: '保存成功', icon: 'none' })
+    await useUserStore.getInfo()
+  } catch (err) {
+    uni.showToast({ title: err?.msg || '保存失败', icon: 'none' })
+  }
+}
+
+</script>
+<style lang="scss" scoped>
+.scrollBox {
+  width: 100vw;
+  // height: 100vh;
+  height: calc(100vh - 30rpx);
+  margin-bottom: 30rpx;
+}
+.content {
+  padding: 30rpx;
+}
+
+.changeRole {
+  color: var(--color-666);
+  font-size: 15px;
+  line-height: 26px;
+  margin-bottom: 40rpx;
+}
+.upload-img{
+  position: relative;
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+}
+.upload-file{
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 10rpx;
+}
+</style>

+ 64 - 0
layout/index.vue

@@ -0,0 +1,64 @@
+<!--  -->
+<template>
+  <view class="page-app" >
+    <view class="page-main">
+      <view class="page-body">
+        <!-- 页面内容插槽 -->
+        <slot />
+      </view>
+    </view>
+    
+    <view class="page-modal">
+      <!-- 全局授权弹窗 -->
+      <authModal />
+      <!-- 全局分享弹窗 -->
+      <!-- <s-share-modal :shareInfo="shareInfo" /> -->
+      <!-- 全局快捷入口 -->
+      <!-- <s-menu-tools /> -->
+    </view>
+  </view>
+</template>
+
+<script setup>
+import authModal from './components/auth-modal.vue'
+import { useIM } from '@/hooks/useIM'
+import { watch } from 'vue'
+import { userStore } from '@/store/user'
+const { resetConfig } = useIM()
+const useUserStore = userStore()
+watch(() => useUserStore?.accountInfo?.userId, (newVal, oldVal) => {
+  if (useUserStore.refreshToken) {
+		// 监听登录状态
+    resetConfig()
+	}
+})
+
+</script>
+
+<style lang="scss" scoped>
+.page-app {
+  position: relative;
+  // color: var(--ui-TC);
+  // background-color: var(--ui-BG-1) !important;
+  // z-index: 2;
+  display: flex;
+  width: 100%;
+  height: 100vh;
+
+  .page-main {
+    position: absolute;
+    z-index: 1;
+    width: 100%;
+    min-height: 100%;
+    display: flex;
+    flex-direction: column;
+
+    .page-body {
+      width: 100%;
+      position: relative;
+      z-index: 1;
+      flex: 1;
+    }
+  }
+}
+</style>

+ 0 - 1
main.js

@@ -23,7 +23,6 @@ pinia.use(piniaPluginPersistedstate)
 
 export function createApp() {
   const app = createSSRApp(App)
-
   app.use(pinia)
 
   return {

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 144 - 143
package-lock.json


+ 4 - 1
package.json

@@ -1,11 +1,14 @@
 {
   "dependencies": {
     "dayjs": "^1.11.13",
+    "js-base64": "^3.7.7",
     "lodash-es": "^4.17.21",
     "luch-request": "^3.1.1",
     "pinia": "^2.2.2",
     "pinia-plugin-persist-uni": "^1.3.1",
     "pinia-plugin-persistedstate": "^4.0.1",
-    "qs": "^6.13.0"
+    "qs": "^6.13.0",
+    "js-sha256": "^0.11.0",
+    "wukongimjssdk": "^1.2.10"
   }
 }

+ 310 - 7
pages.json

@@ -3,8 +3,10 @@
 		{
 			"path": "pages/index/resume",
 			"style": {
-				"navigationBarTitleText": "门墩儿招聘",
-				"enablePullDownRefresh": true
+				"navigationBarTitleText": "门墩儿",
+				"enablePullDownRefresh": true,
+				"backgroundColorBottom":"#f4f4f5",
+				"navigationStyle": "custom" 
 			}
 		},
 		{
@@ -28,7 +30,13 @@
 		{
 			"path": "pages/index/communicate",
 			"style": {
-				"navigationBarTitleText": "沟通"
+				"navigationBarTitleText": "最近联系人"
+			}
+		},
+		{
+			"path": "pages/addWebView/index",
+			"style": {
+				"navigationBarTitleText": ""
 			}
 		}
 	],
@@ -36,12 +44,114 @@
 		{
 			"root": "pagesA",
 			"pages": [
+				{
+					"path": "resumeAnalysis/index",
+					"style": {
+						"navigationBarTitleText": "简历解析"
+					}
+				},
+				{
+					"path": "resumeOnline/index",
+					"style": {
+						"navigationBarTitleText": "在线简历"
+					}
+				},
+				{
+					"path": "resumeOnline/baseInfoEdit",
+					"style": {
+						"navigationBarTitleText": "基本信息"
+					}
+				},
+				{
+					"path": "resumeOnline/portrait",
+					"style": {
+						"navigationBarTitleText": "个人画像标签选择"
+					}
+				},
+				{
+					"path": "resumeOnline/advantage",
+					"style": {
+						"navigationBarTitleText": "个人优势"
+					}
+				},
+				{
+					"path": "resumeOnline/jobIntention",
+					"style": {
+						"navigationBarTitleText": "求职意向"
+					}
+				},
+				{
+					"path": "resumeOnline/educationExp",
+					"style": {
+						"navigationBarTitleText": "教育经历"
+					}
+				},
+				{
+					"path": "resumeOnline/workExperience",
+					"style": {
+						"navigationBarTitleText": "工作经验"
+					}
+				},
+				{
+					"path": "resumeOnline/trainingExperience",
+					"style": {
+						"navigationBarTitleText": "培训经历"
+					}
+				},
+				{
+					"path": "resumeOnline/vocationalSkills",
+					"style": {
+						"navigationBarTitleText": "职业技能"
+					}
+				},
 				{
 					"path": "resume/index",
 					"style": {
 						"navigationBarTitleText": "附件简历"
 					}
 				},
+				{
+					"path": "student/index",
+					"style": {
+						"navigationBarTitleText": "学生专区"
+					}
+				},
+				{
+					"path": "student/information",
+					"style": {
+						"navigationBarTitleText": "学生信息"
+					}
+				},
+				{
+					"path": "student/internshipRecord",
+					"style": {
+						"navigationBarTitleText": "实习记录"
+					}
+				},
+				{
+					"path": "student/internshipReport",
+					"style": {
+						"navigationBarTitleText": "实习报告"
+					}
+				},
+				{
+					"path": "student/addReport",
+					"style": {
+						"navigationBarTitleText": "新增实习报告"
+					}
+				},
+				{
+					"path": "student/certificateDetail",
+					"style": {
+						"navigationBarTitleText": "实习证书详情"
+					}
+				},
+				{
+					"path": "student/internshipButler",
+					"style": {
+						"navigationBarTitleText": "实习管家"
+					}
+				},
 				{
 					"path": "collect/index",
 					"style": {
@@ -65,6 +175,60 @@
 					"style": {
 						"navigationBarTitleText": "个人信息"
 					}
+				},
+				{
+					"path": "recommendation/index",
+					"style": {
+						"navigationBarTitleText": "我的推荐"
+					}
+				},
+				{
+					"path": "chart/index",
+					"style": {
+						"navigationBarTitleText": "我的聊天"
+					}
+				},
+				{
+					"path": "coupon/index",
+					"style": {
+						"navigationBarTitleText": "我的优惠券"
+					}
+				},
+				{
+					"path": "balance/index",
+					"style": {
+						"navigationBarTitleText": "我的余额"
+					}
+				},
+				{
+					"path": "integral/index",
+					"style": {
+						"navigationBarTitleText": "积分明细"
+					}
+				},
+				{
+					"path": "vip/index",
+					"style": {
+						"navigationBarTitleText": "vip权益"
+					}
+				},
+				{
+					"path": "vipPackage/index",
+					"style": {
+						"navigationBarTitleText": "会员套餐"
+					}
+				},
+				{
+					"path": "vip/template/index",
+					"style": {
+						"navigationBarTitleText": "简历模板"
+					}
+				},
+				{
+					"path": "vip/blockEnt/index",
+					"style": {
+						"navigationBarTitleText": "屏蔽企业"
+					}
 				}
 			]
 		},
@@ -77,28 +241,155 @@
 						"navigationBarTitleText": "企业详情"
 					}
 				},
+				{
+					"path": "contactUs/index",
+					"style": {
+						"navigationBarTitleText": "联系我们"
+					}
+				},
 				{
 					"path": "positionDetail/index",
 					"style": {
 						"navigationBarTitleText": "职位详情"
 					}
+				},
+				{
+					"path": "inviteRecord/index",
+					"style": {
+						"navigationBarTitleText": "新用户邀请记录"
+					}
+				},
+				{
+					"path": "agreement/index",
+					"style": {
+						"navigationBarTitleText": "协议中心"
+					}
+				},
+				{
+					"path": "agreement/user",
+					"style": {
+						"navigationBarTitleText": "用户协议"
+					}
+				},
+				{
+					"path": "agreement/privacy",
+					"style": {
+						"navigationBarTitleText": "隐私协议"
+					}
+				},
+				{
+					"path": "agreement/CopyrightPolicy",
+					"style": {
+						"navigationBarTitleText": "版权政策"
+					}
+				},
+				{
+					"path": "agreement/WorkplaceCommunityPolicy",
+					"style": {
+						"navigationBarTitleText": "职场社区政策"
+					}
+				},
+				{
+					"path": "agreement/UserBehaviorNorms",
+					"style": {
+						"navigationBarTitleText": "用户行为规范"
+					}
+				},
+				{
+					"path": "recommendEnterprise/index",
+					"style": {
+						"navigationBarTitleText": "精选企业"
+					}
+				},
+				{
+					"path": "sharePoster/index",
+					"style": {
+						"navigationBarTitleText": "我的分享码"
+					}
+				},
+				{
+					"path": "headhunting/index",
+					"style": {
+						"navigationBarTitleText": "门墩儿猎头"
+					}
+				},
+				{
+					"path": "headhunting/pages/details",
+					"style": {
+						"navigationBarTitleText": "门墩儿猎头资质"
+					}
+				},
+				{
+					"path": "headhunting/pages/service",
+					"style": {
+						"navigationBarTitleText": "我们的服务"
+					}
+				},
+				{
+					"path": "headhunting/pages/contact",
+					"style": {
+						"navigationBarTitleText": "联系我们"
+					}
+				},
+				{
+					"path": "about/index",
+					"style": {
+						"navigationBarTitleText": "联系我们"
+					}
+				},
+				{
+					"path": "preferredGroup/index",
+					"style": {
+						"navigationBarTitleText": "优选集团"
+					}
+				},
+				{
+					"path": "jobFair/index",
+					"style": {
+						"navigationBarTitleText": "招聘会"
+					}
+				},
+				{
+					"path": "jobFair/jobFairShare",
+					"style": {
+						"navigationBarTitleText": "分享招聘会"
+					}
+				},
+				{
+					"path": "jobFair/jobFairEntShare",
+					"style": {
+						"navigationBarTitleText": "分享招聘会企业"
+					}
+				},
+				{
+					"path": "jobFair/enterprisesClassification",
+					"style": {
+						"navigationBarTitleText": "招聘会/企业"
+					}
+				},
+				{
+					"path": "jobFair/positionClassification",
+					"style": {
+						"navigationBarTitleText": "招聘会/职位"
+					}
 				}
 			]
 		}
 	],
 	"globalStyle": {
 		"navigationBarTextStyle": "black",
-		"navigationBarTitleText": "门墩儿招聘",
+		"navigationBarTitleText": "门墩儿",
 		"navigationBarBackgroundColor": "#ffffff",
 		"backgroundColor": "#ffffff"
 	},
 	"tabBar": {
-		"color": "#000",
-		"selectedColor": "#00897B",
+		"color": "#7A7E83",
+		"selectedColor": "#00B760",
 		"borderStyle": "black",
 		"backgroundColor": "#ffffff",
 		"height": "65px",
 		"fontSize": "24rpx",
+		"custom": true,
 		"list": [
 			{
 				"pagePath": "pages/index/resume",
@@ -108,7 +399,7 @@
 			},
 			{
 				"pagePath": "pages/index/position",
-				"text": "职",
+				"text": "职",
 				"iconPath": "/static/img/position.png",
 				"selectedIconPath": "/static/img/position-fill.png"
 			},
@@ -118,6 +409,18 @@
 				"iconPath": "/static/img/message.png",
 				"selectedIconPath": "/static/img/message-fill.png"
 			},
+			{
+				"pagePath": "pages/index/welfare",
+				"text": "会员福利",
+				"iconPath": "/static/img/welfare.png",
+				"selectedIconPath": "/static/img/welfare.png"
+			},
+			{
+				"pagePath": "pages/index/crowdsourcing",
+				"text": "赏金",
+				"iconPath": "/static/img/pin.png",
+				"selectedIconPath": "/static/img/pin-fill.png"
+			},
 			{
 				"pagePath": "pages/index/my",
 				"text": "企业",

+ 27 - 0
pages/addWebView/index.vue

@@ -0,0 +1,27 @@
+<template>
+  <web-view :src="url"></web-view>
+</template>
+ 
+<script setup>
+import { ref } from 'vue'
+import { onLoad, onShareAppMessage } from '@dcloudio/uni-app'
+
+const url = ref('')
+
+onLoad((options) => {
+  // 传入需要跳转的链接 使用web-view标签进行跳转
+  url.value = decodeURIComponent(options.url)
+
+  wx.showShareMenu({
+    withShareTicket: true,
+    menus: ['shareAppMessage']
+  })
+  onShareAppMessage(() => {
+    return {
+      title: options?.title || '门墩儿 专注顶尖招聘',
+      path: '/pages/addWebView/index?url=' + encodeURIComponent(url.value)
+    }
+  })
+})
+
+</script>

+ 218 - 4
pages/index/communicate.vue

@@ -1,12 +1,226 @@
 <template>
-  <view class="nodata-img-parent">
-    <image src="https://minio.citupro.com/dev/static/nodata.png" mode="widthFix" style="width: 100vw;height: 100vh;"></image>
-  </view>
+  <layout-page>
+		<view class="height defaultBgc">
+			<scroll-view class="scrollBox" scroll-y="true" >
+				<view class="box" v-for="item in items" :key="item.id" @tap="handleTo(item)">
+					<view class="box-header">
+						<template v-if="item.unread === '0'">
+							<image
+								class="enterAvatar"
+								:src="getUserAvatar(item?.userInfoVo?.userInfoResp?.avatar, item?.userInfoVo?.userInfoResp?.sex, !item?.userInfoVo && item.channel_id === 'system' ? true : false)"
+							></image>
+						</template>
+						<template v-else>
+							<uni-badge class="uni-badge-left-margin" :text="item.unread" absolute="rightTop" size="small">
+								<image
+									class="enterAvatar"
+									:src="getUserAvatar(item?.userInfoVo?.userInfoResp?.avatar, item?.userInfoVo?.userInfoResp?.sex, !item?.userInfoVo && item.channel_id === 'system' ? true : false)"
+								></image>
+							</uni-badge>
+						</template>
+					</view>
+					<view class="box-content">
+						<view class="box-content-names">
+							<view class="name">
+								{{ item.thatName }}
+								<text class="nameSub">{{ formatName(item.enterpriseAnotherName) }}</text>
+								<span class="line" v-if="item.postNameCn && item.enterpriseAnotherName"></span>
+								<text class="nameSub">{{ item.postNameCn }}</text>
+							</view>
+						</view>
+						<view class="box-content-text">{{ timesTampChange(+item.timestamp.padEnd(13, '0')) }}</view>
+					</view>
+				</view>
+				<image
+					v-if=" items.length===0 "
+					src="https://minio.citupro.com/dev/static/nodata.png"
+					mode="widthFix"
+					style="width: 100%;">
+				</image>
+				<uni-load-more status="noMore" />
+			</scroll-view>
+		</view>
+  </layout-page>
 </template>
 
 <script setup>
+import { ref, watch } from 'vue'
+import layoutPage from '@/layout'
+import { getConversationSync } from '@/api/common'
+import { onShow, onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
+import { getUserAvatar } from '@/utils/avatar'
+import { timesTampChange } from '@/utils/date'
+import { userStore } from '@/store/user'
+import { useIMStore } from '@/store/im'
+import { formatName } from '@/utils/getText'
+
+const IM = useIMStore()
+
+const useUserStore = userStore()
+
+const items = ref([])
+watch([() => useUserStore.refreshToken, () => IM.newMsg], () => {
+	// 检测实例是否存在
+	if (!IM.uid?.value) {
+		return
+	}
+	init()
+})
+
+watch(() => IM.uid, (val) => {
+	if (!val) {
+		return
+	}
+	// 监听uid变化
+	init()
+})
+
+onShow(() => {
+	const currentPage = getCurrentPages()[0];  // 获取当前页面实例
+	const currentTabBar = currentPage?.getTabBar?.();
+
+	// 设置当前tab页的下标index
+	currentTabBar?.setData({ selected: 1 });
+	init()
+})
+onLoad(() => {
+  wx.showShareMenu({
+    withShareTicket: true,
+    menus: ['shareAppMessage', 'shareTimeline']
+  })
+  onShareAppMessage(() => {
+    return {
+      title: '门墩儿 专注顶尖招聘',
+      path: '/pages/index/position',
+			imageUrl: '../../static/img/share-poster.jpg'
+    }
+  })
+	onShareTimeline(() => {
+    return {
+      title: '门墩儿 专注顶尖招聘',
+      path: '/pages/index/position',
+			imageUrl: '../../static/img/share-poster.jpg'
+    }
+  })
+})
+
+
+
+const handleTo = (item) => {
+	const { userInfoVo, thatName, postNameCn, enterpriseAnotherName, channel_id, channel_type } = item
+	const query = {
+		id: userInfoVo?.userInfoResp?.userId,
+		name: thatName,
+		postName: postNameCn,
+		enterpriseName: formatName(enterpriseAnotherName),
+		enterpriseId: userInfoVo?.userInfoResp?.enterpriseId,
+		channelID: channel_id,
+		channelType: channel_type,
+		avatar: userInfoVo?.userInfoResp?.avatar,
+		sex: userInfoVo?.userInfoResp?.sex,
+	}
+	const queryStr = Object.keys(query).reduce((r, v) => {
+		if (!query[v]) {
+			return r
+		}
+		return r += `${v}=${encodeURIComponent(query[v])}&`
+	}, '?')
+	uni.navigateTo({
+    url: `/pagesA/chart/index${queryStr.slice(0, -1)}`
+  })
+}
+
+async function init () {
+	try {
+		const { data } = await getConversationSync({ msg_count: 1 })
+		if (!data) {
+			return
+		}
+		items.value = data.map(item => {
+			return {
+				thatName: item?.userInfoVo ? (item.userInfoVo.userInfoResp?.name ? item.userInfoVo.userInfoResp.name : '游客') : '系统消息',
+				enterpriseAnotherName: item?.userInfoVo?.userInfoResp?.enterpriseAnotherName || item?.userInfoVo?.userInfoResp?.enterpriseName,
+				postNameCn: item?.userInfoVo?.userInfoResp?.postNameCn ?? '',
+				...item
+			}
+		})
+	} catch (error) {
+
+	}
+}
+
+
+
 </script>
 
 <style scoped lang="scss">
-
+.scrollBox {
+	padding-bottom: 24rpx;
+  box-sizing: border-box;
+}
+.box {
+	background: #FFF;
+	height: 130rpx;
+	padding: 20rpx;
+	box-sizing: border-box;
+	display: flex;
+	border-bottom: 2rpx solid #eee;
+	&-header {
+		width: 120rpx;
+		height: 100%;
+		position: relative;
+	}
+	&-content {
+		flex: 1;
+		width: 0;
+		display: flex;
+		flex-direction: column;
+		justify-content: space-between;
+		&-names {
+			display: flex;
+			justify-content: space-between;
+			
+			.name {
+				flex: 1;
+				overflow: hidden;
+				white-space: nowrap;
+				text-overflow: ellipsis;
+				.nameSub {
+					font-size: 0.75em;
+					color: #999;
+				}
+				.line {
+					display: inline-block;
+					width: 2rpx;
+					height: 24rpx;
+					vertical-align: middle;
+					background-color: #e0e0e0;
+					margin: 0 6rpx;
+				}
+			}
+			.time {
+				color: #999;
+				font-size: .85em;
+			}
+		}
+		&-text {
+			color: #999;
+			font-size: .85em;
+			overflow: hidden;
+			white-space: nowrap;
+			text-overflow: ellipsis;
+		}
+	}
+}
+.height {
+	height: 100vh;
+	padding-bottom: 120rpx;
+	box-sizing: border-box;
+}
+.enterAvatar{
+	width: 80rpx;
+	height: 80rpx;
+	border-radius: 50%;
+	margin: 0 auto;
+}
 </style>

+ 305 - 0
pages/index/crowdsourcing.vue

@@ -0,0 +1,305 @@
+<template>
+  <layout-page>
+		<view class="content defaultBgc">
+			<view class="content-top">
+				<view class="content-top-title">
+					<view class="content-top-title-label">
+						<text class="content-top-title-label-l">全员猎寻</text>
+						<text class="content-top-title-label-s">海量岗位 | 推荐有赏</text>
+					</view>
+				</view>
+			</view>
+			<scroll-view class="scrollBox" scroll-y="true" @scrolltolower="loadingMore" style="position:relative;">
+				<view class="content-top">
+					<view v-if="swiperAdList.length" class="content-top-carousel">
+						<view class="content-top-carousel-box">
+							<SwiperAd :list="swiperAdList" imgUrlKey="img" :strType="false" @click="handleToDetails"></SwiperAd>
+						</view>
+					</view>
+					<view class="content-top-recommend" :class="{'ss-m-t-20': !swiperAdList.length}">
+						<view class="content-top-recommend-box">
+							<resume-status>
+								<template #header>
+									<view class="content-top-recommend-box-title">
+										<text class="title">我的推荐</text>
+									</view>
+								</template>
+							</resume-status>
+						</view>
+					</view>
+				</view>
+				<view class="content-main">
+					<view class="content-main-list">
+						<PositionList :list="items" :noMore="false"></PositionList>
+						<uni-load-more :status="more" />
+					</view>
+				</view>
+			</scroll-view>
+		</view>
+  </layout-page>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue'
+import SwiperAd from '@/components/SwiperAd'
+import layoutPage from '@/layout'
+import ResumeStatus from '@/components/ResumeStatus'
+import { getJobAdvertisedHire } from '@/api/position.js'
+import { dealDictArrayData } from '@/utils/position.js'
+import PositionList from '@/components/PositionList'
+import { getWebContent } from '@/api/common'
+import { formatName } from '@/utils/getText'
+import { onShow, onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
+// 设置自定义tabbar选中值
+onShow(() => {
+    const currentPage = getCurrentPages()[0];  // 获取当前页面实例
+    const currentTabBar = currentPage?.getTabBar?.();
+
+    // 设置当前tab页的下标index
+    currentTabBar?.setData({ selected: 3 });
+})
+
+// 获取轮播图
+const swiperAdList = ref([])
+const getSystemWebContent = async () => {
+  const { data } = await getWebContent()
+  swiperAdList.value = data.appHomeCarousel || []
+}
+getSystemWebContent()
+
+const items = reactive([])
+const pageInfo = ref({
+	pageNo: 1,
+	pageSize: 20
+})
+
+const loading = ref(false)
+const total = ref(0)
+
+const more = ref('more')
+
+// 跳转企业详情
+const handleToDetails = ({ link, title }) => {
+	if (link) {
+		uni.navigateTo({ url: `/pages/addWebView/index?url=${link}&title=${title || '风尚榜奖投票'}` })
+	}
+}
+
+onLoad(() => {
+	getList()
+  wx.showShareMenu({
+    withShareTicket: true,
+    menus: ['shareAppMessage', 'shareTimeline']
+  })
+  onShareAppMessage(() => {
+    return {
+      title: '门墩儿 专注顶尖招聘',
+      path: '/pages/index/position',
+			imageUrl: '../../static/img/share-poster.jpg'
+    }
+  })
+	onShareTimeline(() => {
+		return {
+			title: '门墩儿 专注顶尖招聘',
+			path: '/pages/index/position',
+			imageUrl: '../../static/img/share-poster.jpg'
+		}
+	})
+})
+
+const loadingMore = (e) => {
+	if (total.value === items.length) {
+		return
+	}
+	if (loading.value) {
+		return
+	}
+	more.value = 'loading'
+	pageInfo.value.pageNo++
+	getList()
+}
+
+async function getList () {
+	loading.value = true
+	try {
+		const { data } = await getJobAdvertisedHire({
+			...pageInfo.value
+		})
+		if (!data?.list) {
+			pageInfo.pageNo--
+			return
+		}
+		const _items = dealDictArrayData([], data.list)
+		items.push(..._items.map(e => {
+			return {
+				job: e,
+				enterprise: {
+					welfareList: e.tagList,
+					logoUrl: e.logoUrl,
+					anotherName: formatName(e.anotherName || e.name),
+					industryName: e.industryName,
+					scaleName: e.scaleName
+				}
+			}
+		}))
+		total.value = +data.total
+		more.value = items.length === total.value ? 'noMore' : 'more'
+	} catch (error) {
+		more.value = more
+		pageInfo.pageNo--
+	} finally {
+		loading.value = false
+	}
+	
+}
+
+
+</script>
+
+<style scoped lang="scss">
+$defaultColor: #999;
+@mixin box {
+	// border-radius: 24rpx;
+	width: 100%;
+	height: 100%;
+	background: #FFF;
+	overflow: hidden;
+}
+.content {
+	height: 100vh;
+	display: flex;
+	flex-direction: column;
+	&-top {		
+		&-title {
+			padding: 24rpx;
+			box-sizing: border-box;
+			width: 100%;
+			display: flex;
+			justify-content: space-between;
+			align-items: flex-end;
+			background-color: #FFF;
+			&-label {
+				display: flex;
+				align-items: flex-end;
+				&-l {
+					font-size: 46rpx;
+					margin-right: 16rpx;
+				}
+				&-s {
+					font-size: 24rpx;
+					color: $defaultColor;
+				}
+			}
+			&-local {
+				color: $defaultColor;
+				display: flex;
+				align-items: flex-end;
+			}
+		}
+		// &-carousel {
+		// 	padding: 24rpx;
+		// }
+		&-recommend {
+			padding: 0 24rpx;
+			&-box {
+				@include box;
+				&-title {
+					display: flex;
+					justify-content: space-between;
+					align-items: flex-end;
+					padding: 20rpx;
+					font-size: 30rpx;
+					.route {
+						color: $defaultColor;
+						font-size: .75em;
+					}
+				}
+			}
+		}
+	}
+	&-main {
+		&-filter {
+			padding: 24rpx;
+			font-size: 30rpx;
+			color: $defaultColor;
+			display: flex;
+			justify-content: space-between;
+			&-type{
+				&-item {
+					padding: 20rpx;
+					&.active {
+						color: #000;
+					}
+				}
+			}
+		}
+		&-list {
+			width: 100%;
+			// padding: 0 24rpx 24rpx 24rpx;
+			box-sizing: border-box;
+			&-box {
+				padding: 24rpx;
+				margin-bottom: 24rpx;
+				box-sizing: border-box;
+				@include box;
+				.top {
+					width: 100%;
+					display: flex;
+					justify-content: space-between;
+					.title {
+						flex: 1;
+						overflow: hidden; /* 隐藏超出部分 */  
+						white-space: nowrap; /* 不换行 */  
+						text-overflow: ellipsis; /* 超出部分显示省略号 */  
+					}
+					.remuneration {
+						color: aquamarine;
+					}
+				}
+				.main {
+					display: flex;
+					font-size: 24rpx;
+					margin: 28rpx 0;
+					.tag {
+						&.blue {
+							background: aliceblue;
+							color: royalblue;
+						}
+						font-size: 20rpx;
+						border-radius: 8rpx;
+						color: #666;
+						background: #eee;
+						padding: 6rpx 10rpx;
+						margin-right: 20rpx;
+					}
+				}
+				.bottom {
+					color: #666;
+					display: flex;
+					justify-content: space-between;
+					.origin {
+						font-size: 0.8em;
+						.interval {
+							padding: 0 16rpx;
+						}
+					}
+					.local {
+						color: #aaa;
+						font-size: 0.6em;
+					}
+				}
+			}
+			.noMore {
+				font-size: 24rpx;
+				color: $defaultColor;
+				text-align: center;
+			}
+		}
+	}
+}
+.scrollBox {
+	height: 0;
+	flex: 1;
+	padding-bottom: 120rpx;
+}
+</style>

+ 305 - 58
pages/index/my.vue

@@ -1,83 +1,232 @@
 <template>
-  <view class="ss-p-b-30">
-    <view class="text-center" @tap="handleTap">
-      <img :src="getUserAvatar(baseInfo?.avatar, baseInfo?.sex)" alt="" class="img-box">
-      <view v-if="!useUserStore.isLogin" class="font-weight-bold font-size-20">点击登录</view>
-      <view v-else class="font-weight-bold font-size-20">{{ baseInfo?.name || userInfo?.phone }}</view>
-    </view>
-    <view class="d-flex" style="margin-top: 80rpx;">
-      <view v-for="(item, index) in itemList" :key="index" @tap="handleToLink(item)" class="parent">
-				<view class="d-flex justify-space-between">
-					<view>
-						<view class="colors">
-							<span>查看更多</span>
-							<uni-icons color="#c8c5c4" type="right" size="15" class="ml"/>
-						</view>
-						<view class="size-16">{{ item.title }}</view>
-					</view>
-					<view>
-						<uni-icons color="#00897B" :type="item.icon" size="45"/>
+  <layout-page>
+		<view class="pb-150">
+			<view class="text-center" :class="vip ? 'vipBox' : 'avatarBox'" @tap="handleTap">
+				<img :src="getUserAvatar(baseInfo?.avatar, baseInfo?.sex)" alt="" class="img-box">
+				<image v-if="vip" src="/static/svg/vip.svg" class="vipIcon" @click.stop="handleToLink({path: '/pagesA/vip/index'})"></image>
+				<view v-if="!useUserStore.isLogin" class="font-weight-bold font-size-20">点击登录</view>
+				<view v-else class="font-weight-bold font-size-20">{{ baseInfo?.name || userInfo?.phone }}</view>
+			</view>
+
+			<view style="padding: 40rpx 0 0 0;">
+				<resume-status></resume-status>
+			</view>
+			
+			<view class="d-flex">
+				<view v-for="(item, index) in itemList" :key="index" @tap="handleToLink(item)" class="parent">
+					<view class="d-flex justify-space-around align-center">
+						<uni-icons color="#00B760" :type="item.icon" size="30"/>
+						<text class="itemText">{{ item.title }}</text>
 					</view>
 				</view>
 			</view>
-    </view>
-
-    <view style="height: 10rpx; background-color: #f8f8fa;"></view>
-
-    <view class="card">
-      <uni-list>
-        <uni-list-item v-for="item in list" :clickable="true" :key="item.title" :title="item.title" showArrow :rightText="item.rightTex || ''" @click="handleToLink(item)"></uni-list-item>
-      </uni-list>
-    </view>
-
-    <button v-if="useUserStore.isLogin" class="send-button" @tap="handleLogout">退出登录</button>
+	
+			<view style="height: 20rpx; background-color: #f8f8fa;"></view>
+	
+			<view class="card">
+				<uni-list>
+					<uni-list-item
+						v-for="item in list"
+						:clickable="true"
+						:key="item.title"
+						:title="item.title"
+						showArrow
+						:rightText="item.rightTex || ''"
+						@click="handleToLink(item)"
+					>
+					</uni-list-item>
+				</uni-list>
+			</view>
+	
+			<button v-if="useUserStore.isLogin" class="send-button" style="margin-bottom: 50px;" @tap="handleLogout">退出登录</button>
+	
+			<uni-popup ref="popup" type="dialog">
+				<uni-popup-dialog type="warn" cancelText="取消" confirmText="确定" title="系统提示" content="确认退出账号?" @confirm="handleLogoutConfirm"
+					@close="handleLogoutClose"></uni-popup-dialog>
+			</uni-popup>
+			<!-- 我的分享码 -->
+			<uni-popup ref="shareQrCodePopup" type="dialog">
+				<view class="shareQrCodePopupContent">
+					<view class="color-primary text">邀请用户注册领50积分</view>
+					<view class="qrCode">
+						<image
+            	v-if="!!shareUrl"
+							:src="shareUrl"
+							:show-menu-by-longpress="true"
+							style="width: 200px;height: 200px; padding: 20rpx;"
+						></image>
+					</view>
+					<view v-if="shareUrl" class="saveImg">长按二维码保存图片</view>
+				</view>
+			</uni-popup>
 
-    <uni-popup ref="popup" type="dialog">
-			<uni-popup-dialog type="warn" cancelText="取消" confirmText="确定" title="系统提示" content="确认退出账号?" @confirm="handleLogoutConfirm"
-				@close="handleLogoutClose"></uni-popup-dialog>
-		</uni-popup>
-  </view>
+			<uni-popup ref="inputDialog" type="dialog">
+				<view class="shareQrCodePopupContent">
+					<view>请前往网页版{{ dialogType ? '门墩儿招聘' : '门墩儿商城' }}</view>
+					<uni-link class="ss-m-t-10" :href="dialogType ? 'https://www.menduner.com' : 'https://www.menduner.com/mall'" text="点击复制网页地址" color="#00B760" fontSize="16" copyTips="已复制,请在电脑端打开"></uni-link>
+				</view>
+			</uni-popup>
+		</view>
+	</layout-page>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
+import { ref, computed, watch } from 'vue'
+import ResumeStatus from '@/components/ResumeStatus'
 import { userStore } from '@/store/user'
 import { getUserAvatar } from '@/utils/avatar'
-import { getAccessToken } from '@/utils/request'
+import { getAccessToken, showNecessaryInfoPopup } from '@/utils/request'
+import layoutPage from '@/layout'
+import { showAuthModal } from '@/hooks/useModal'
+import { onShow, onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
+import { getJobAdvertisedShareQrcode } from '@/api/user'
+// 设置自定义tabbar选中值
+onShow(() => {
+    const currentPage = getCurrentPages()[0];  // 获取当前页面实例
+    const currentTabBar = currentPage?.getTabBar?.();
+
+    // 设置当前tab页的下标index
+    currentTabBar?.setData({ selected: 4 });
+})
+
+// showAuthModal('') //测试 selectUserType
 
+const inputDialog = ref()
 const useUserStore = userStore()
 const baseInfo = computed(() => useUserStore?.baseInfo)
 const userInfo = computed(() => useUserStore?.userInfo)
+const vip = computed(() => new Date().getTime() < useUserStore?.userInfo?.vipExpireDate)
+
 const popup = ref()
+const shareQrCodePopup = ref()
 const itemList = [
-	{ title: "面试管理", path: "/pagesA/interview/index", icon: "list" },
-	{ title:'谁看过我', path:'/pagesA/seenMe/index', icon:'staff' }
+	{ title: "我的关注", path: "/pagesA/collect/index", icon: "list" },
+	{ title:'关注我的', path:'/pagesA/seenMe/index', icon:'staff' }
 ]
 
-const list = [
+const defaultList = [
+	{	title: '在线简历',	path: '/pagesA/resumeOnline/index'	},					
 	{	title: '附件简历',	path: '/pagesA/resume/index'	},					
-	{ title: '我的收藏', path: '/pagesA/collect/index' },
-	{ title: '前往门墩儿甄选商城', appId: 'wx6decdf12f9e7a061' },
-	{ title: '切换为求职者', rightTex: '我要找工作' }
+	{ title: '学生专区', path: '/pagesA/student/index', key: 'student', defaultHide: true },
+	{ title: '面试管理', path: '/pagesA/interview/index' },
+	{	title: '会员套餐', path: '/pagesA/vipPackage/index'	},		
+	{	title: '我的分享码',	path: '/pagesB/sharePoster/index'	},					
+	{	title: '新用户邀请记录',	path: '/pagesB/inviteRecord/index'	},
+	{ title: '门墩儿商城', appId: 'wx6decdf12f9e7a061' },
+	{ title: '我要招聘', key: 'recruit' },
+	{ title: '联系我们', path: '/pagesB/contactUs/index' },
+	{ title: '协议中心', path: '/pagesB/agreement/index', open: true }
 ]
+const list = ref(defaultList.filter(e => !e.defaultHide))
+
+watch(
+  () => baseInfo.value, 
+  (newVal) => {
+		if (newVal) {
+			list.value = defaultList.map(e => { // 不改变原数组,退出登录需要重置菜单
+				let hide = e.defaultHide
+				if (e?.key === 'student' && newVal.type === '1') hide = false // 学生信息管理。 type:0是求职者,1是学生
+				return hide ? null : e
+			}).filter(Boolean)
+		}
+  },
+  { immediate: true },
+  // { deep: true }
+)
+
+watch(
+  () => vip.value, 
+  (newVal) => {
+		if (newVal) list.value.splice(3, 0, {	title: 'vip权益', key: 'vip',	path: '/pagesA/vip/index'	})
+		else list.value = list.value.filter(e => !e.key || e.key !== 'vip')
+  },
+  { immediate: true },
+  // { deep: true }
+)
+
+onLoad(() => {
+  wx.showShareMenu({
+    withShareTicket: true,
+    menus: ['shareAppMessage', 'shareTimeline']
+  })
+  onShareAppMessage(() => {
+    return {
+      title: '门墩儿 专注顶尖招聘',
+      path: '/pages/index/position',
+			imageUrl: '../../static/img/share-poster.jpg'
+    }
+  })
+	onShareTimeline(() => {
+    return {
+      title: '门墩儿 专注顶尖招聘',
+      path: '/pages/index/position',
+			imageUrl: '../../static/img/share-poster.jpg'
+    }
+  })
+})
+
+const openSubscribe = (path) => {
+  wx.requestSubscribeMessage({
+    tmplIds: ['2dByiI4wf6D3ZmxDH1QywH264F3N-9ysnvsS7xQm4PE'],
+    success:(res)=>{
+      console.log(res, 'uni.requestSubscribeMessage-res')
+			uni.navigateTo({
+				url: path
+			})
+    },
+    fail:(err)=>{
+      console.log('订阅失败', err)
+			uni.navigateTo({
+				url: path
+			})
+    }
+  })
+}
 
 // 列表跳转
+const dialogType = ref('')
 const handleToLink = (item) => {
+	if (item.open && item.path) return uni.navigateTo({ url: item.path })
 	if (item.appId) {
-		uni.navigateToMiniProgram({
-			appId: item.appId,
-			// extraData: {} // 要传递的数据
-		})
-		return
+		// uni.navigateToMiniProgram({
+		// 	appId: item.appId,
+		// })
+		// return
+		// uni.showToast({
+		// 	title: '请前往网页版门墩儿商城',
+		// 	icon: 'none'
+		// })
+		dialogType.value = 0
+		inputDialog.value.open()
+	}
+	if (item.key === 'recruit') {
+		dialogType.value = 1
+		inputDialog.value.open()
 	}
 	if (!item.path) return
   if (!getAccessToken()) {
 		uni.showToast({
-			title: '请先登录'
+			title: '请先登录',
+			icon: 'none'
 		})
-		uni.navigateTo({
-			url: '/pages/login/index'
+		showAuthModal()
+		return
+	}
+  if (showNecessaryInfoPopup()) {
+		uni.showToast({
+			title: '请先完善基本信息',
+			icon: 'none'
 		})
+		showAuthModal('necessaryInfo')
+		return
+	}
+  if (item.path === 'shareQrCode') {
+		handleShareCode()
+		return
+	}
+	// 点击在线简历需调起订阅消息
+	if (item.title === '在线简历'){
+		openSubscribe(item.path)
 		return
 	}
 	uni.navigateTo({
@@ -85,16 +234,74 @@ const handleToLink = (item) => {
 	})
 }
 
+const shareUrl = ref()
+// 生成分享二维码
+const handleShareCode = async () => {
+	const userId = useUserStore?.accountInfo?.userId || ''
+	if (!userId) {
+		uni.showToast({
+			title: '请先登录',
+			icon: 'none'
+		})
+		showAuthModal()
+		return
+	}
+  if (showNecessaryInfoPopup()) {
+		uni.showToast({
+			title: '请先完善基本信息',
+			icon: 'none'
+		})
+		showAuthModal('necessaryInfo')
+		return
+	}
+	const query = {
+		scene: 'shareId=' + userId,
+		path: 'pages/login/index',
+		width: 200,
+		autoColor: false,
+		checkPath: true,
+		hyaline: true
+	}
+	const res = await getJobAdvertisedShareQrcode(query)
+	shareUrl.value = res?.data ? 'data:image/png;base64,' + res.data : ''
+	shareQrCodePopup.value.open()
+}
+
+// 保存到本地
+// const handleSaveShareUrl = async () => {
+// 	const fsm = wx.getFileSystemManager()
+// 	const data = shareUrl.value
+// 	if (!data) return
+// 	fsm.writeFile({
+// 		filePath:wx.env.USER_DATA_PATH+'/MySharingCode.png',
+// 		data: data.slice(22),
+// 		encoding:'base64',
+// 		success: res => {
+// 			wx.saveImageToPhotosAlbum({
+// 				filePath: wx.env.USER_DATA_PATH + '/MySharingCode.png',
+// 				success: function (res) {
+// 					wx.showToast({
+// 						title: '保存成功',
+// 					})
+// 				},
+// 				fail: function (err) {
+// 					console.log(err)
+// 				}
+// 			})
+// 		}, fail: err => {
+// 			console.log(err)
+// 		}
+// 	})
+// }
+
 // 登录
 const handleTap = () => {
 	if (!useUserStore.isLogin) {
-		uni.navigateTo({
-			url: '/pages/login/index'
-		})
+		showAuthModal()
 		return
 	}
 	uni.navigateTo({
-		url: '/pagesA/info/index'
+		url: '/pagesA/resumeOnline/index'
 	})
 }
 
@@ -106,22 +313,40 @@ const handleLogoutClose = () => {
   popup.value.close()
 }
 const handleLogoutConfirm = () => {
+	list.value = defaultList.filter(e => !e.hide) // 重置菜单
   useUserStore.handleLogout()
 }
 </script>
 
 <style scoped lang="scss">
+.pb-150 {
+	padding-bottom: 100px;
+}
 .img-box {
   width: 150rpx;
   height: 150rpx;
   border: 2rpx solid #ccc;
   border-radius: 50%;
 }
-::v-deep .uni-list-item{
-	height: 140rpx !important;
-	line-height: 140rpx !important;
+.vipBox {
+	color: #a18a0f;
+	position: relative;
+	.img-box {
+    border: 1px solid gold;
+	}
+	.vipIcon {
+		position: absolute;
+		width: 45px;
+		height: 24px;
+		bottom: 34px;
+		left: 50%;
+	}
+}
+:deep(.uni-list-item) {
+	height: 120rpx !important;
+	line-height: 120rpx !important;
 }
-::v-deep .uni-list-item__content-title{
+:deep(.uni-list-item__content-title) {
 	font-size: 32rpx !important;
 	font-weight: 500;
 }
@@ -134,6 +359,10 @@ const handleLogoutConfirm = () => {
 	font-size: 32rpx;
 	margin: 13rpx 0 5rpx 0;
 }
+.itemText {
+	color: #3f424f;
+	font-size: 36rpx;
+}
 .parent{
 	width: 50%;
 	border: 1px solid #f4f4f4;
@@ -144,4 +373,22 @@ const handleLogoutConfirm = () => {
 .parent:first-child{
 	margin: 0 0 20rpx 30rpx;
 }
+.shareQrCodePopupContent {
+	width: 75vw;
+	padding: 40rpx;
+	margin-bottom: 20rpx;
+	text-align: center;
+	background-color: #fff;
+	.text {
+		margin-bottom: 20px;
+	}
+	.qrCode {
+		margin-left: 4px;
+	}
+	.saveImg {
+		text-align: center;
+		margin-top: 10px;
+		color: #999;
+	}
+}
 </style>

+ 4 - 0
pages/index/position.vue

@@ -39,5 +39,9 @@ const fabClick = () => {
 .box {
   height: 100vh;
   overflow: hidden;
+  padding-bottom: 120rpx;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
 }
 </style>

+ 483 - 0
pages/index/welfare.vue

@@ -0,0 +1,483 @@
+<template>
+  <layout-page>
+		<view class="box defaultBgc">
+      <scroll-view class="scrollBox" scroll-y="true">
+        <view class="content">
+          <!-- 钱包 -->
+          <view class="wallet">
+            <view class="wallet-content">
+              <view class="wallet-content-member">
+                <view class="iconBox">
+                  <view class="iconBox-line">
+                    <uni-icons type="vip-filled" size="20" color="#ffbc00"></uni-icons>
+                  </view>
+                </view>
+                <view class="name">
+                  <text v-if="useUserStore.isLogin" @tap="toInfo" class="active">
+                    {{ useUserStore?.baseInfo?.name || useUserStore?.userInfo?.phone }}
+                  </text>
+                  <text v-else @tap="handleLogin">点击登录</text>
+                </view>
+                </view>
+              <view class="wallet-content-integral wallet-content-item" @tap="handleTo('integral')">
+                <text>{{ balance.point || 0 }}</text>
+                <text class="title">积分</text>
+              </view>
+              <view class="wallet-content-cash wallet-content-item" @tap="handleTo('balance')">
+                <text>{{ balance.balance > 0 ? (balance?.balance / 100.0).toFixed(2) : 0 }}</text>
+                <text class="title">余额</text>
+              </view>
+            </view>
+          </view>
+          <!-- 签到 -->
+          <!-- <view class="signIn">
+            <view class="signIn-content">
+              <view class="signIn-content-items">
+                <view class="item" v-for="(sign, i) in SignItems" :key="sign.day">
+                  <view class="box" :class="{ active: i < continuousDay }">
+                    <uni-icons type="vip" size="30" color="#ffbc00" style="text-align: center;"></uni-icons>
+                    <text class="text">+{{ sign.point }}</text>
+                  </view>
+                  <view class="day">
+                    <text>{{ i === todayNumber ? '今' : '第' + sign.day }}天</text>
+                  </view>
+                </view>
+              </view>
+              <view class="tips">
+                <button
+                  :disabled="todaySignIn"
+                  :class="{ disabled: todaySignIn }"
+                  @tap="handleSignIn"
+                >
+                  {{ todaySignIn ? '已签到' : '签到'}}
+                </button>
+              </view>
+            </view>
+          </view> -->
+          <!-- 积分规则、积分兑换 -->
+          <view class="ss-m-t-20">
+            <uni-section title="积分规则" type="line"  style="background-color: #fff;">
+              <uni-list>
+                <uni-list-item v-for="(k, i) in integralRules" :key="i" :showArrow="false">
+                  <template v-slot:header>
+                    <text class="ss-m-t-10">{{ k.title }}</text>
+                  </template>
+                  <template v-slot:footer>
+                    <view class="d-flex align-center" style="width: 130px; text-align: start; position: relative;">
+                      <image src="/static/svg/integral.svg" style="width: 30px; height: 30px;"></image>
+                      <view class="color-primary ss-m-l-10">{{ k.point }}</view>
+                      <uni-tag :inverted="true" :text="k.complete ? '已完成' : '未完成'" :type="k.complete ? 'success' : 'error'" style="position: absolute; right: 0;" />
+                    </view>
+                  </template>
+                </uni-list-item>
+              </uni-list>
+            </uni-section>
+            <view class="ss-m-t-20">
+              <uni-section title="积分兑换" type="line" style="background-color: #fff;">
+                <view class="goods" >
+                  <view class="goods-item" v-for="(item,index) in goodsList" :key="index" @tap.stop="handleClickGoods">
+                    <image :src="item.url" class="goods-item-img" mode="widthFix"></image>
+                    <view class="goods-item-name">{{ item.name }}</view>
+                    <view>消耗积分<text class="goods-item-price">{{ item.point }}</text></view>
+                  </view>
+                </view>
+              </uni-section>
+            </view>
+          </view>
+        </view>
+      </scroll-view>
+		</view>
+
+    <uni-popup ref="inputDialog" type="dialog">
+      <view class="shareQrCodePopupContent">
+        <view class="ss-m-b-10">请前往网页版门墩儿商城兑换</view>
+        <uni-link href="https://www.menduner.com/mall" text="点击复制网页地址" color="#00B760" fontSize="16" copyTips="已复制,请在电脑端打开"></uni-link>
+      </view>
+    </uni-popup>
+	</layout-page>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+import layoutPage from '@/layout'
+import {
+	// getRewardSignInConfigList,
+	// getRewardSignInRecordSummary,
+  // createRewardSignInRecord,
+  getAccountBalance,
+  getUserAccount,
+} from '@/api/sign'
+import { getTaskList } from '@/api/integral'
+import { onShow, onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
+import { userStore } from '@/store/user'
+import { showAuthModal } from '@/hooks/useModal'
+import { showNecessaryInfoPopup } from '@/utils/request'
+
+const inputDialog = ref()
+const useUserStore = userStore()
+// 设置自定义tabBar选中值
+
+// const SignItems = ref([])
+// 积分规则
+const integralRules = ref([])
+const getTask = async () => {
+  const { data } = await getTaskList({ mark: '推荐任务', type: 0 })
+  integralRules.value = data
+}
+// 商品列表
+const goodsList = [
+  { name: '房券-高端酒店房券', point: 12000, url: 'https://minio.menduner.com/dev/menduner/hotalRoomVoucher.png' },
+  { name: '门墩儿酒店英语学习年卡', point: 8000, url: 'https://minio.menduner.com/dev/menduner/englishCourses.jpg' },
+  { name: '红酒-经典年份葡萄酒', point: 5000, url: 'https://minio.menduner.com/dev/menduner/redWine.png' },
+  { name: '瑞幸咖啡券-瑞幸咖啡精致享受券', point: 2000, url: 'https://minio.menduner.com/dev/menduner/coffee.png' },
+  { name: '减压捏捏乐', point: 500, url: 'https://minio.menduner.com/dev/menduner/pinchMusic.png' }
+]
+
+onLoad(() => {
+  wx.showShareMenu({
+    withShareTicket: true,
+    menus: ['shareAppMessage', 'shareTimeline']
+  })
+  onShareAppMessage(() => {
+    return {
+      title: '门墩儿 专注顶尖招聘',
+      path: '/pages/index/position',
+      imageUrl: '../../static/img/share-poster.jpg'
+    }
+  })
+  onShareTimeline(() => {
+    return {
+      title: '门墩儿 专注顶尖招聘',
+      path: '/pages/index/position',
+      imageUrl: '../../static/img/share-poster.jpg'
+    }
+  })
+})
+
+const handleClickGoods = () => {
+  // uni.showToast({
+  //   icon: 'none',
+  //   title: '请前往网页版门墩儿商城兑换'
+  // })
+  inputDialog.value.open()
+}
+
+// 连续签到天数
+// const continuousDay = ref(0)
+// 今天有无签到
+// const todaySignIn = ref(false)
+
+// 今天
+// const todayNumber = ref()
+
+const balance = ref({})
+
+watch([() => useUserStore.refreshToken, () => useUserStore.isLogin], () => {
+  if (useUserStore.isLogin) {
+    // getSummary()
+    getBalance()
+  }
+})
+
+onShow(() => {
+  const currentPage = getCurrentPages()[0];  // 获取当前页面实例
+  const currentTabBar = currentPage?.getTabBar?.();
+  // 设置当前tab页的下标index
+  currentTabBar?.setData({ selected: 2 });
+  init()
+})
+
+function init () {
+  // 获取签到列表
+  // getSignIn()
+  // 获取签到信息
+  // getSummary()
+  // 获取余额积分
+  getBalance()
+  // 获取积分规则列表
+  getTask()
+}
+
+// 获取积分余额
+async function getBalance() {
+  const { data } = await getAccountBalance()
+  const { data: _data } = await getUserAccount()
+  balance.value = {
+    ...data,
+    point: _data?.point
+  }
+}
+
+
+// 获取个人签到统计
+// async function getSummary() {
+//   const { data } = await getRewardSignInRecordSummary()
+//   if (!data) return
+//   continuousDay.value = data.continuousDay // 连续签到第n天
+//   todaySignIn.value = data.todaySignIn // 今天有无签到
+//   todayNumber.value = todaySignIn.value ? continuousDay.value - 1 : continuousDay.value
+// }
+// 获取签到列表
+// async function getSignIn () {
+//   try {
+//     const { data } = await getRewardSignInConfigList()
+//     SignItems.value = data
+//   } catch (error) {
+    
+//   }
+// }
+// 签到
+// async function handleSignIn () {
+//   uni.showLoading({
+//     title: '正在签到'
+//   })
+//   try {
+//     const { code } = await createRewardSignInRecord()
+//     if (code !== 0) {
+//       return
+//     }
+//     setTimeout(async () => {
+//       const { code: _code } = await getSummary()
+//       if (_code !== 0) {
+//         return
+//       }
+//       uni.showToast({
+//         title: '签到成功',
+//         icon: 'success',
+//         mask: true
+//       })
+//       // 更新积分
+//       getBalance()
+//     }, 1000)
+//   } catch (error) {
+    
+//   } finally {
+//     uni.hideLoading()
+//   }
+// }
+
+function handleLogin () {
+  if (!useUserStore.isLogin) {
+		showAuthModal()
+		return
+	}
+}
+
+function toInfo () {
+  uni.navigateTo({
+		url: '/pagesA/info/index'
+	})
+}
+
+function handleTo (page) {
+  if (!useUserStore.isLogin) {
+		showAuthModal()
+		return
+  }
+  if (showNecessaryInfoPopup()) {
+		uni.showToast({
+			title: '请先完善基本信息',
+			icon: 'none'
+		})
+		showAuthModal('necessaryInfo')
+		return
+	}
+  uni.navigateTo({
+    url: `/pagesA/${page}/index`
+  })
+}
+</script>
+
+<style scoped lang="scss">
+.shareQrCodePopupContent {
+	width: 75vw;
+	padding: 40rpx;
+	margin-bottom: 20rpx;
+	text-align: center;
+	background-color: #fff;
+}
+.box {
+  overflow: hidden;
+  height: 100vh;
+  padding: 20rpx 0 0 0;
+  box-sizing: border-box;
+  .scrollBox{
+    height: 100%;
+    
+    padding-bottom: 120rpx;
+    box-sizing: border-box;
+  }
+  .content {
+    padding-bottom: 24rpx;
+    box-sizing: border-box;
+  }
+  .wallet {
+    box-sizing: border-box;
+    height: 200rpx;
+    margin-bottom: 20rpx;
+    &-content {
+      display: flex;
+      height: 100%;
+      border-radius: 10rpx;
+      background: #FFF;
+      
+      &-member {
+        width: 50%;
+        display: flex;
+        align-items: center;
+        position: relative;
+        &:after {
+          content: '';
+          position: absolute;
+          width: 4rpx;
+          height: 30%;
+          right: 0;
+          top: 35%;
+          background: #00B760;
+        }
+        .iconBox {
+          width: 100rpx;
+          height: 100%;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          &-line {
+            border: 4rpx solid #ffbc00;
+            border-radius: 180rpx;
+            padding: 10rpx;
+          }
+        }
+        .name {
+          flex: 1;
+          .actvie {
+            color: #ffbc00;
+          }
+          
+        }
+      }
+      &-item {
+        width: 25%;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        .title {
+          font-size: .85em;
+        }
+      }
+      &-integral {
+      }
+      &-cash {
+        
+      }
+    }
+  }
+  .signIn {
+    // padding: 0 20rpx;
+    box-sizing: border-box;
+    &-content {
+      border-radius: 10rpx;
+      background: #FFF;
+      &-items {
+        padding: 20rpx 20rpx;
+        display: flex;
+        height: 220rpx;
+        box-sizing: border-box;
+        .item {
+          height: 140rpx;
+          width: 100%;
+          align-items: center;
+          justify-content: center;
+          padding: 0 10rpx;
+          .box {
+            height: 100%;
+            width: 100%;
+            display: flex;
+            flex-direction: column;
+            background: #f2f4f7;
+            border-radius: 10rpx;
+            &.active {
+              color: #fff;
+              background: rgba(16,137,123, .66);
+            }
+            .text {
+              font-size: .75em;
+              text-align: center;
+            }
+          }
+          .day {
+            margin-top: 10rpx;
+            height: 60rpx;
+            text-align: center;
+            font-size: .5em;
+          }
+        }
+      }
+      .tips {
+        display: flex;
+        align-items: center;
+        justify-content: flex-end;
+        height: 80rpx;
+        font-size: .75em;
+        color: #999;
+        &-light {
+          color: #00B760;
+          padding: 0 10rpx;
+        }
+        button {
+          width: 180rpx;
+          height: 60rpx;
+          font-size: 24rpx;
+          margin: 0 10rpx;
+          border: unset;
+          background-color: #00B760;
+          color: #FFF;
+          &.disabled {
+            // background-color: #a7a7a7 !important;
+            opacity: .5;
+          }
+        }
+      }
+      
+    }
+  }
+  .goods {
+    padding: 0 15upx;
+		display: flex;
+		flex-wrap: wrap;
+		justify-content: space-between;
+    &-item {
+      background: #FFFFFF;
+      width: 48%;
+      margin: 10upx 0;
+      box-sizing: border-box;
+      &-img {
+        width: 100%;
+        height: 250upx;
+        display: block;
+        margin: auto;
+      }
+      &-price {
+        padding-top: 10upx;
+        color: #00B760;
+        font-size: 32upx;
+      }
+      &-name {
+        width: 100%;
+        white-space: nowrap;
+        font-size: 28upx;
+        line-height: 50upx;
+        padding-bottom: 10upx;
+        padding-top: 10upx;
+        overflow:hidden; 
+        text-overflow:ellipsis;
+        -webkit-box-orient:vertical;
+        -webkit-line-clamp:2; 
+      }
+    }
+  }
+}
+:deep(.uni-section .uni-section-header__decoration) {
+  background-color: #00B760 !important;
+}
+</style>

+ 192 - 49
pages/login/index.vue

@@ -1,13 +1,11 @@
 <template>
   <view class="ss-p-30 head-box">
-    <view class="head-title">欢迎来到门墩儿招聘</view>
-    <uni-segmented-control class="ss-m-t-60" :current="current" :values="items" style-type="text" active-color="#00897B" @clickItem="onClickItem" />
-    <view class="head-subtitle ss-m-t-10">未注册的手机号,验证后自动注册账号</view>
+    <view class="head-title">欢迎来到门墩儿,新用户注册领积分</view>
+    <view class="head-subtitle ss-m-t-30 justify-center">未注册的手机号,验证后自动注册账号</view>
 
-    <view class="ss-m-t-30">
+    <view class="ss-m-t-60">
       <!-- 短信验证码登录 -->
       <uni-forms
-        v-if="current === 0"
         ref="smsLoginRef"
         v-model="state.sms"
         :rules="state.smsRules"
@@ -29,69 +27,139 @@
           <uni-easyinput placeholder="请输入验证码" v-model="state.sms.code" :inputBorder="false" type="number" maxlength="6"></uni-easyinput>
         </uni-forms-item>
       </uni-forms>
+      <!-- <view>
+        <button v-if="!protocol" class="wxLogon" type="text" :plain="true" @click="showProtocolToast">微信一键登录</button>
+        <button v-else class="wxLogon" type="text" :plain="true" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">微信一键登录</button>
+      </view> -->
 
-      <!-- 账号密码登录 -->
-      <uni-forms
-        v-else
-        ref="accountLoginRef"
-        v-model="state.account"
-        :rules="state.rules"
-        validateTrigger="bind"
-        labelWidth="140"
-        labelAlign="center"
-      >
-        <uni-forms-item name="phone" label="账号">
-          <uni-easyinput placeholder="请输入账号" v-model="state.account.phone" :inputBorder="false"></uni-easyinput>
-        </uni-forms-item>
+      <button class="send-button" @tap="handleLogin"> 注册 </button>
+      <view class="agreement-box ss-flex ss-row-center">
+        <uni-icons size="20" :type="protocol ? 'checkbox-filled' : 'circle'" :color="protocol ? '#00B760' : '#ccc'" @tap="protocol = !protocol"></uni-icons>
+        <view class="color-999 ss-flex ss-col-center ss-m-l-8 font-size-13">
+          我已阅读并遵守
+          <view class="color-primary" @tap.stop="handleToDetail('user')">
+            《用户协议》
+          </view>
+          <view class="agreement-text">和</view>
+          <view class="color-primary" @tap.stop="handleToDetail('privacy')">
+            《隐私协议》
+          </view>
+        </view>
+      </view>
+    </view>
 
-        <uni-forms-item name="password" label="密码">
-          <uni-easyinput type="password" placeholder="请输入密码" v-model="state.account.password" :inputBorder="false"></uni-easyinput>
-        </uni-forms-item>
-      </uni-forms>
+    <!-- <AdvertisePop></AdvertisePop> -->
 
-      <button class="send-button" @tap="handleLogin"> 登录/注册 </button>
-    </view>
+    <!-- <uni-popup ref="popup" background-color="#fff" type="bottom" :is-mask-click="false">
+      <view style="padding: 20px;">
+        <view class="text-center ss-m-b-50 font-size-20 color-primary">请完善您的基本信息</view>
+        <uni-forms
+          ref="baseInfoRef"
+          v-model="formData"
+          :rules="formRules"
+          validateTrigger="bind"
+          label-width="75px"
+          labelAlign="center"
+        >
+          <uni-forms-item name="name" label="姓名" required>
+            <uni-easyinput placeholder="请输入姓名" v-model="formData.name" :inputBorder="false" type="text"></uni-easyinput>
+          </uni-forms-item>
+          <uni-forms-item name="phone" label="联系电话" required>
+            <uni-easyinput placeholder="请输入联系电话" v-model="formData.phone" :inputBorder="false" type="number"></uni-easyinput>
+          </uni-forms-item>
+          <uni-forms-item name="sex" label="性别" required>
+            <uni-data-picker v-model="formData.sex" :localdata="dictObj.sex" :clear-icon="false" popup-title="请选择性别" :map="{ text: 'label', value: 'value' }"></uni-data-picker>
+          </uni-forms-item>
+          <uni-forms-item name="jobStatus" label="求职状态" required>
+            <uni-data-picker v-model="formData.jobStatus" :localdata="dictObj.jobStatus" :clear-icon="false" popup-title="请选择求职状态" :map="{ text: 'label', value: 'value' }"></uni-data-picker>
+          </uni-forms-item>
+          <uni-forms-item name="expType" label="工作经验" required>
+            <uni-data-picker v-model="formData.expType" :localdata="dictObj.exp" :clear-icon="false" popup-title="请选择工作经验" :clear="false" :map="{ text: 'label', value: 'value' }"></uni-data-picker>
+          </uni-forms-item>
+          <uni-forms-item name="eduType" label="最高学历" required>
+            <uni-data-picker v-model="formData.eduType" :localdata="dictObj.edu" :clear-icon="false" popup-title="请选择最高学历" :clear="false" :map="{ text: 'label', value: 'value' }"></uni-data-picker>
+          </uni-forms-item>
+        </uni-forms>
+      </view>
+      <view class="f-horizon-center">
+				<button type="primary" size="default" class="send-button"  @click="submit">提 交</button>
+			</view>
+		</uni-popup> -->
   </view>
 </template>
 
 <script setup>
+// 扫码登录注册
 import { ref, unref } from 'vue'
-import { mobile, password, code } from '@/utils/validate'
+import { mobile, code } from '@/utils/validate'
 import { getSmsCode, getSmsTimer } from '@/utils/code'
 import { userStore } from '@/store/user'
+import { onLoad } from '@dcloudio/uni-app'
+import { useIM } from '@/hooks/useIM'
+import { watch } from 'vue'
+// import AdvertisePop from '@/components/Advertisement'
+// import { dictObj } from '@/utils/position.js'
+// import { savePersonSimpleInfo } from '@/api/user'
+// import { showAuthModal } from '@/hooks/useModal'
 
 const useUserStore = userStore()
-const items = ['短信登录', '账号登录']
-const current = ref(1)
-const accountLoginRef = ref()
 const smsLoginRef = ref()
+const protocol = ref(false)
+// const popup = ref()
+// const baseInfoRef = ref()
+// const formData = ref({})
 const state = ref({
   isMobileEnd: false, // 手机号输入完毕
   codeText: '获取验证码',
   sms: {
-    phone: '13229740092',
-    code: ''
-  },
-  account: {
-    phone: '13229740091',
-    password: 'Citu123'
-  },
-  rules: {
-    phone: mobile,
-    password,
+    phone: '',
+    code: '',
+    inviteCode: ''
   },
   smsRules: {
     code,
     phone: mobile
   }
 })
+// const formRules = {
+// 	name:{
+// 		rules: [{required: true, errorMessage: '请输入姓名' }]
+// 	},
+//   phone:{
+// 		rules: [{required: true, errorMessage: '请输入联系电话' }]
+// 	},
+// 	sex : {
+// 		rules: [{required: true, errorMessage: '请选择您的性别' }]
+// 	},
+//   expType: {
+// 		rules: [{required: true, errorMessage: '请选择您的工作年限' }]
+// 	},
+//   eduType: {
+// 		rules: [{required: true, errorMessage: '请选择您的最高学历' }]
+// 	},
+//   jobStatus: {
+// 		rules: [{required: true, errorMessage: '请选择您的求职状态' }]
+// 	}
+// }
 
-// 登录成功后的返回页面
-const backUrl = getCurrentPages().length ? getCurrentPages()[getCurrentPages().length - 2].route : ''
+const { resetConfig } = useIM()
+watch(() => useUserStore?.accountInfo?.userId, (newVal, oldVal) => {
+  if (useUserStore.refreshToken) {
+		// 监听登录状态
+    resetConfig()
+	}
+})
 
-const onClickItem = (e) => {
-  current.value = e.currentIndex
-}
+onLoad((options) => {
+  console.log(options, 'options-my-share=========')
+  // const testOptions = { scene: "shareId%3D1" }
+	if (options.scene) {
+    const scene = decodeURIComponent(options.scene)
+    const shareUserId = scene.split('=')[1]
+    state.value.sms.inviteCode = shareUserId
+    console.log(shareUserId, 'shareUserId')
+  }
+})
 
 // 获取验证码
 const handleCode = () => {
@@ -106,30 +174,105 @@ const handleCode = () => {
   getSmsCode('smsLogin', state.value.sms.phone)
 }
 
+// 查看协议详情
+const handleToDetail = (type) => {
+  const url = type === 'user' ? '/pagesB/agreement/user' : '/pagesB/agreement/privacy'
+  uni.navigateTo({
+    url
+  })
+}
+
 // 登录
 const handleLogin = async () => {
-  const validate = await unref(current.value === 0 ? smsLoginRef : accountLoginRef).validate()
+  if (!protocol.value) return uni.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
+  const validate = await unref(smsLoginRef).validate()
   if (!validate) return
-  await useUserStore.handleSmsLogin(current.value === 0 ? state.value.sms : state.value.account, current.value === 0 ? true : false, backUrl || 'pages/index/my')
+  if (!state.value.sms.inviteCode) {
+    uni.showToast({
+      title: '邀请码缺失,请重新扫码',
+      icon: 'none'
+    })
+    return
+  }
+  const result = await useUserStore.handleShareUserRegister(state.value.sms)
+  if (!result || !Object.keys(result).length) return
+  uni.switchTab({
+    url: '/pages/index/my'
+  })
+  
+  // uni.showToast({ icon: 'none', title: '请先完善信息' })
+  // formData.value.phone = state.value.sms.phone
+  // popup.value.open()
 }
+
+// const submit = async () => {
+//   const validate = await unref(baseInfoRef).validate()
+//   if (!validate) return uni.showToast({ title: '请将信息补充完整', icon: 'none' })
+//   try {
+//     uni.showToast({ title: '保存成功', icon: 'none' })
+//     await useUserStore.getInfo()
+//     await useUserStore.getUserInfo()
+//     uni.switchTab({
+//       url: '/pages/index/position'
+//     })
+//   } catch (err) {
+//     uni.showToast({ title: err.msg || '保存失败', icon: 'none' })
+//   }
+// }
+
+
+// const showProtocolToast = () => {
+//   uni.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
+// }
+
+// // 微信登录
+// const getPhoneNumber = async (e) => {
+//   if (e?.detail?.errMsg !== 'getPhoneNumber:ok') {
+//     uni.showToast({ title: '微信登录失败', icon: 'none' })
+//     return
+//   }
+//   changeType.value = 'login'
+//   wx.login({
+//     success: async (result) => {
+//       const wxLoginCode = result?.code || ''
+//       const query = {
+//         loginCode: wxLoginCode,
+//         phoneCode: e.detail.code,
+//         state: e.detail.encryptedData,
+//       }
+//       await useUserStore.handleSmsLogin(query, 2)
+//     },
+//     fail:(res)=> { console.log("获取登录凭证code失败!", res) }
+//   })
+// }
 </script>
 
 <style scoped lang="scss">
 .login-code {
   width: 73px;
   min-width: 73px;
-  color: #00897B;
+  color: #00B760;
   text-align: center; 
   font-size: 12px; 
   cursor: pointer;
-  border: 1px dashed #00897B;
+  border: 1px dashed #00B760;
   border-radius: 26px;
   padding: 0;
 }
 .head-title {
   font-size: 40rpx;
   text-align: center;
-  color: #00897B;
-  margin-bottom: 100rpx;
+  color: #00B760;
+}
+.wxLogon {
+  text-align: center;
+  font-size: .85em;
+  color: #00B760;
+  border: none;
+  margin: 0;
+  padding: 0;
+  // margin: 40rpx;
+  margin-top: -20px;
+  margin-bottom: -15px;
 }
 </style>

+ 146 - 0
pagesA/balance/index.vue

@@ -0,0 +1,146 @@
+<template>
+  <layout-page>
+    <view class="box defaultBgc">
+      <scroll-view class="scrollBox" scroll-y="true" @scrolltolower="loadingMore">
+        <view>
+          <view class="panel">
+            <view>
+              <uni-icons color="#f30" type="icon-renminbi1688" size="16" custom-prefix="iconfont"></uni-icons>
+              <text class="text">{{ balance.balance > 0 ? (balance?.balance / 100.0).toFixed(2) : 0 }}</text>
+            </view>
+            <view>
+              <button class="btn">充值</button>
+            </view>
+          </view>
+          <view class="list">
+            <uni-list border-full>
+              <uni-list-item
+                v-for="(item, index) in items"
+                :key="index"
+                :title="item.title"
+                :note="item.price"
+                :rightText="item.createTime"
+              />
+            </uni-list>
+            <uni-load-more :status="more" />
+          </view>
+        </view>
+      </scroll-view>
+    </view>
+  </layout-page>
+</template>
+<!-- balance 余额 -->
+<script setup>
+import { ref } from 'vue'
+import {
+  getAccountBalance,
+  getUserWalletTransactionPage
+} from '@/api/sign'
+import { timesTampChange } from '@/utils/date'
+
+const balance = ref({})
+
+const items = ref([])
+const pageInfo = ref({
+  pageNo: 1,
+  pageSize: 20
+})
+const total = ref(0)
+const more = ref('more')
+
+getBalance()
+getList()
+// 获取积分余额
+async function getBalance() {
+  const { data } = await getAccountBalance()
+  if (!data) {
+    return
+  }
+  balance.value = data
+}
+
+async function getList () {
+  try {
+    const { data } = await getUserWalletTransactionPage(pageInfo.value)
+    if (!data || !data.list || !data.list.length) {
+      if (pageInfo.value.pageNo === 1) {
+        return
+      }
+      pageInfo.value.pageNo--
+      more.value = 'more'
+      return
+    }
+    const _data = data.list.map(e => {
+      return {
+        // ...e,
+        // _payPrice: (e.payPrice / 100.0).toFixed(2),
+        // _payTime: timesTampChange(e.payTime)
+        // ...e,
+        title: e.title,
+        createTime: timesTampChange(e.createTime),
+        price: (e.price / 100.0).toFixed(2)
+      }
+    })
+    items.value.push(..._data)
+    total.value = +data.total
+    more.value = total.value <= items.value.length ? 'noMore' : 'more'
+  } catch (error) {
+    if (pageInfo.value.pageNo === 1) {
+      return
+    }
+    pageInfo.value.pageNo--
+    more.value = 'more'
+  }
+}
+function loadingMore () {
+  if (more.value === 'noMore') {
+    return
+  }
+  more.value = 'loading'
+  pageInfo.value.pageNo++
+  getList()
+}
+</script>
+
+<style lang="scss" scoped>
+.box {
+  height: 100vh;
+}
+.scrollBox {
+  width: 100vw;
+  // padding: 20rpx;
+  box-sizing: border-box;
+  .panel {
+    position: sticky;
+    z-index: 2;
+    top: 0;
+    width: 100%;
+    padding: 20rpx;
+    margin-bottom: 20rpx;
+    box-sizing: border-box;
+    background: #FFF;
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-end;
+    box-shadow: 0px 0px 10px 0px rgb(0, 0, 0, 0.25);
+    .text {
+      color: #F30;
+      font-size: 54rpx;
+
+    }
+    .btn {
+      width: 180rpx;
+      height: 60rpx;
+      line-height: 60rpx;
+      font-size: 28rpx;
+      text-align: center;
+      background: #00B760;
+      color: #FFF;
+      border-radius: 30rpx;
+    }
+  }
+  .list {
+    // padding: 20rpx;
+  }
+}
+</style>

+ 1012 - 0
pagesA/chart/index.vue

@@ -0,0 +1,1012 @@
+<template>
+  <view class="box">
+    <view class="box-top">
+      <view class="box-top-title">
+        {{ info.name }}
+        <text class="subText">
+          {{ info?.postName && info?.postName !== 'null' && info?.postName !== 'undefined' ? info.postName : '' }}
+          <text v-if="info?.postName && info?.postName !== 'null' && info?.postName !== 'undefined' && info.enterpriseName" class="gun">|</text>
+          {{ formatName(info.enterpriseName) }}
+        </text>
+      </view>
+      <!-- <view class="box-top-content" v-if="interview.length">
+        <view v-for="val in interview" :key="val.id" class="color-666">
+          <view class="box-top-content-t">
+            <view class="font-weight-bold color-primary">
+              <text>{{ val.job.name }}</text>
+              <text v-if="!val.job.payFrom && !val.job.payTo" class="ml-3">面议</text>
+              <text v-else class="ml-3">{{ val.job.payFrom ? val.job.payFrom + '-' : '' }}{{ val.job.payTo }}</text>
+            </view>
+            <view :style="`color: ${val.statusColor};`" >
+              {{ val.statusText }}
+            </view>
+          </view>
+          <view class="mt-1 font-size-14 ellipsis" style="max-width: 100%;">
+            <view class="py-1">面试时间:{{ timesTampChange(val.time, 'Y-M-D h:m') }}</view>
+            <view class="py-1">面试地点:{{ val.address }}</view>
+            <view class="py-1">联系电话:{{ val.invitePhone }}</view>
+          </view>
+          <view class="bottom">
+            <view class="tipsText" @click="handleToCenter">在“个人中心-面试”中管理我的面试</view>
+            <view v-if="val.status === '0'" class="btnBox">
+              <button size="mini" type="warn" @click="handleRefuse(val)">拒绝邀请</button>
+              <button size="mini" type="primary" @click="handleAgree(val)">接受邀请</button>
+            </view>
+          </view>
+        </view>
+      </view> -->
+      <uni-notice-bar v-if="hasWaitingProcessingInterview" show-get-more single text="您有待处理的面试邀请,点击查看详情" @click="handleToCenter"/>
+    </view>
+      <!-- newsId 需要和聊天列表里面的id对应 永远取最后列表中的最后一个就可以做到发送消息即时滚动到底部 -->
+      <scroll-view ref="chatRef" :scroll-with-animation="scrollAnimation" :scroll-into-view="newsId" class="box-main" scroll-y="true">
+        <view class="box-main-more" v-if="hasMore">
+          <text @click="handleMore">查看更多</text>
+        </view>
+        <view v-for="(val, index) in items" :key="val.id" :id="'s'+val.id+index">
+          <view class="box-main-time">{{ timesTampChange(+(val.timestamp.padEnd(13, '0'))) }}</view>
+          <template v-if="val.payload?.type === 102">
+            <view class="jobCard">
+              <!-- <view style="display: flex;">
+                <image v-if="info.jobFairId" src="/static/svg/jobFair.svg" class=" ss-m-r-10" style="width: 20px; height: 20px;"></image>
+              </view> -->
+              <view class="jobCard-title"> {{ formatName(val.payload?.content?.positionInfo?.name) }}</view>
+              <view
+                v-if="!val.payload?.content?.positionInfo?.payFrom && !val.payload?.content?.positionInfo?.payTo"
+                class="jobCard-subtitle">
+                薪酬待遇: 面议
+              </view>
+              <view
+                v-else
+                class="jobCard-subtitle"
+              >
+                薪酬待遇: 
+                {{ val.payload?.content?.positionInfo?.payFrom ? val.payload?.content?.positionInfo?.payFrom + ' - ' : '' }}
+                {{ val.payload?.content?.positionInfo?.payTo }}
+              </view>
+              <view class="jobCard-tag">
+                <view
+                  v-for="(v, i) in (val.payload?.content?.positionInfo?.enterprise?.welfareList || [])"
+                  :key="val.message_id + v + i"
+                  style="margin: 10rpx"
+                >
+                  <uni-tag
+                    :text="v"
+                    type="success"
+                  />
+                </view>
+              </view>
+              <view class="jobCard-divider"></view>
+              <view class="jobCard-subtitle text-right">
+                <v-avatar size="24">
+                  <v-img :src="val.payload?.content?.positionInfo?.contact?.avatar"></v-img>
+                </v-avatar>
+                {{ val.payload?.content?.positionInfo?.contact?.name }}
+                {{ val.payload?.content?.positionInfo?.contact?.postNameCn }}
+                {{ formatName(val.payload?.content?.positionInfo?.enterprise?.anotherName || val.payload?.content?.positionInfo?.enterprise?.name) }}
+              </view>
+              <div class="jobCard-subtitle text-right">
+                地址:{{ val.payload?.content?.positionInfo?.address }}
+              </div>
+            </view>
+          </template>
+          <view :class="['message-view_item', val.from_uid === IM.uid ? 'is-self' : 'is-other']">
+            <view class="image">
+              <image
+                :data-target="getUserAvatar(info.avatar, info.sex, info.channelID === 'system' ? true : false)"
+                class="header"
+                :src="(
+                  val.from_uid === IM.uid ?
+                  getUserAvatar(useUserStore.baseInfo?.avatar, useUserStore.baseInfo?.sex) :
+                  getUserAvatar(info.avatar, info.sex, info.channelID === 'system' ? true : false)
+                )"
+              ></image>
+            </view>
+            <!-- 显示沟通职位 -->
+            <template v-if="val.payload?.type === 102">              
+              <view class="message-text" :class="val.from_uid === IM.uid ? 'active' : ''">
+                {{ val.payload?.content.text }}
+              </view>
+            </template>
+            <!-- 发起面试邀请 -->
+            <view class="message-text none" v-else-if="val.payload?.type === 101">
+              <uni-tag text="发起了面试邀请" custom-style="background-color: #00B760; border-color: #00B760; color: #fff;" />
+            </view>
+            <view class="message-text none" v-else-if="val.payload?.type === 103">
+              <uni-tag text="拒绝了面试邀请" type="error" />
+            </view>
+            <view class="message-text none" v-else-if="val.payload?.type === 104">
+              <uni-tag text="接受了面试邀请" custom-style="background-color: #00B760; border-color: #00B760; color: #fff;" />
+            </view>
+            <view v-else-if="val.payload.type === 105" class="text-end">
+              <uni-tag
+                v-if="val.from_uid === IM.uid"
+                :text="val.payload.content?.type === 1 ? '附件简历已发送' : '简历请求已发送'"
+                custom-style="background-color: #00B760; border-color: #00B760; color: #fff;"
+              />
+              <view
+                v-if="val.payload.content?.type !== 2 || val.from_uid !== IM.uid"
+                class="message-text card"
+              >
+                <view class="text-left">
+                  <text v-if="val.payload.content?.type === 1">{{
+                    val.payload.content?.query?.title || '附件简历' }}
+                  </text>
+                  <text v-if="val.payload.content?.type === 2">
+                    我想要一份您的简历,您是否同意
+                  </text>
+                </view>
+                <view class="btn-actions">
+                  <text class="btn" v-if="val.payload.content?.type === 1" @tap="handlePreview(val.payload)">点击预览附件简历</text>
+                  <text class="btn" v-if="val.payload.content?.type === 2" @tap="handleFindResume">点击发送附件简历</text>
+                </view>
+              </view>
+            </view>
+            <view v-else-if="val.payload.type === -1" class="message-text" :class="{ active: val.from_uid === IM.uid}">
+              {{ val.payload?.content?.text }}
+            </view>
+            <view v-else class="message-text" :class="{ active: val.from_uid === IM.uid}">
+              {{ val.payload?.content }}
+            </view>
+          </view>
+        </view>
+      </scroll-view>
+      
+    <view class="box-bottom" v-if="channelItem?.channelID !== 'system'">
+      <view class="box-bottom-tool" style="display: flex; justify-content: space-between;">
+        <text class="toolBtn" :class="{ disabled: !isSendResume }" @tap="handleFindResume">{{ isSendResume ? '简历已投递' : '发送简历' }}</text>
+        <!-- <uni-tag :text="isSendResume ? '简历已投递' : '发送简历'" :disabled="isSendResume" type="success" @tap="handleFindResume"/> -->
+        <!-- <uni-tag text="发 送" type="success" @tap="handleSend"/> -->
+      </view>
+      
+      <view class="d-flex align-end textBox" v-if="channelItem?.channelID !== 'system'">
+        <textarea
+          v-model="inputValue"
+          :cursor-spacing="25"
+          :show-confirm-bar="false"
+          :disable-default-padding="true"
+          confirm-type="send"
+          auto-height
+          @confirm="handleSend"
+        />
+        <text class="submitBtn" @tap="handleSend">发 送</text>
+      </view>
+    </view>
+
+    <uni-popup ref="positionPopup" background-color="#fff">
+      <view style="max-width: 85vw;">
+        <view class="popup-title" style="min-width: 260px;">
+          <text>请选择要投递的职位</text>
+          <uni-icons type="closeempty" size="20" @tap="positionPopupClose"></uni-icons>
+        </view>
+        <view v-for="job in entPositionList" :key="job.value" class="popup-content" @tap="selectJobId = job.value">
+          <view class="iconBox">
+            <uni-icons
+              v-show="selectJobId === job.value"
+              type="checkmarkempty"
+              size="20"
+              :color="selectJobId === job.value ? '#43AC57' : '#999'"></uni-icons>
+          </view>
+          <text class="text" :class="selectJobId === job.value ? 'active' : ''">{{ job.label }}</text>
+        </view>
+        <view v-if="entPositionTotal > 5" class="popup-upload ss-m-x-30" @click="changePositionData">
+          <text style="color: #43AC57;">{{ entPositionListLastData ? '没有更多职位了~ 再选一遍' : '换一批'}}</text>
+        </view>
+      </view>
+      <view class="popup-actions">
+        <button class="default" type="default" @click="selectPositionSubmit">确认</button>
+      </view>
+    </uni-popup>
+
+    <uni-popup ref="popup" background-color="#fff">
+      <view class="popup-title" style="min-width: 260px;">
+        <text>发送简历选择</text>
+        <uni-icons type="closeempty" size="20" @tap="handleClose"></uni-icons>
+      </view>
+      <view v-if="isStudent" class="ss-p-x-50" style="width: 272px;">
+        <studentDeliveryForm ref="studentDeliveryFormRef" />
+      </view>
+      <view v-for="resume in resumeList" :key="resume.id" class="popup-content" @tap="resumeCheck = resume">
+        <view class="iconBox">
+          <uni-icons
+            v-show="resumeCheck.id === resume.id"
+            type="checkmarkempty"
+            size="20"
+            :color="resumeCheck.id === resume.id ? '#43AC57' : '#999'"></uni-icons>
+        </view>
+        <text class="text" :class="resumeCheck.id === resume.id ? 'active' : ''">{{ resume.title }}</text>
+      </view>
+      <view v-if="!resumeList.length" class="popup-upload" @click="handleUploadResume">
+        <view class="popup-upload-box">
+          <uni-icons type="plusempty" size="50" color="#f1f1f1"></uni-icons>
+        </view>
+        <text>温馨提示:您还未上传过简历,点击选取微信聊天文件投递。请在手机上打开此小程序进行文件上传,暂不支持在桌面版小程序中上传文件。</text>
+      </view>
+      <view v-if="resumeList.length" class="popup-actions">
+        <button class="default" type="default" @click="handleSendResume">发送简历</button>
+      </view>
+    </uni-popup>
+
+    <!-- <uni-popup ref="confirm" type="dialog">
+      <uni-popup-dialog
+        :type="isAgree ? 'success' : 'warn'"
+        cancelText="取消"
+        confirmText="确认" 
+        title="系统提示"
+        :content="isAgree ? '确认接受面试吗?' : '确认拒绝面试吗?'"
+        @confirm="handleConfirm"
+        @close="handleCloseConfirm"
+      ></uni-popup-dialog>
+    </uni-popup> -->
+  </view>
+</template>
+
+<script setup>
+import { ref, watch, onMounted, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { useIMStore } from '@/store/im'
+import { userStore } from '@/store/user'
+import { initConnect, send, initChart, getMoreMessages, toChannel } from '@/hooks/useIM'
+import { getDict } from '@/hooks/useDictionaries'
+import { timesTampChange } from '@/utils/date'
+import { getUserAvatar } from '@/utils/avatar'
+import { formatName } from '@/utils/getText'
+import { preview } from '@/utils/preview'
+import { getPersonResumeCv, saveResume } from '@/api/user'
+import { uploadFile } from '@/api/file'
+import { getInterviewInviteListByInviteUserId, getMessageType } from '@/api/common'
+// import { userInterviewInviteReject } from '@/api/personalCenter'
+import {
+  getJobAdvertisedSearch,
+  jobCvRelSend,
+  jobCvRelCheckSend,
+  jobCvRelHireSend
+} from '@/api/position'
+import studentDeliveryForm from '@/components/studentDeliveryForm'
+
+const useUserStore = userStore()
+const IM = useIMStore()
+const info = ref({})
+const chatRef = ref()
+const items = ref([])
+const channelItem = ref(null)
+const hasMore = ref(false)
+
+const popup = ref()
+const resumeCheck = ref({})
+const resumeList = ref([]) // 简历列表
+
+const pageSize = ref(1)
+
+// 求职者面试列表
+const interview = ref([])
+// 求职端-获取求职者与当前邀请人的面试记录
+const statusList = ref([])
+const inputValue = ref('')
+
+// const isAgree = ref(false)
+// const confirm = ref()
+// const chooseInvite = ref(null)
+
+const newsId = ref('') // newsId 需要和聊天列表里面的id对应 永远取最后列表中的最后一个就可以做到发送消息即时滚动到底部
+const scrollAnimation = ref(false)
+
+const isSendResume = ref(false)
+const positionInfo = ref({})
+const isEmployment = ref('-1')
+
+const isStudent = ref(false) // 已测试,待开放上线 
+// const isStudent = ref(useUserStore.baseInfo?.type && Boolean(Number(useUserStore.baseInfo.type) === 1))
+
+onMounted(() => {
+  setTimeout(() => {
+    scrollAnimation.value = true
+  }, 1500)
+})
+
+const {
+  conversationList,
+  // updateConversation,
+  updateUnreadCount,
+  // deleteConversations,
+  resetUnread
+} = initConnect(async (successful) => {
+  if (!successful) {
+    uni.showToast({
+      title: '发送失败',
+      icon: 'none',
+      mask: true,
+    })
+    return
+  }
+  inputValue.value = ''
+  // chatRef.value.reset()
+  // // 发送成功
+  const { list } = await getMoreMessages(1, channelItem.value)
+  // updateConversation()
+  items.value = list.value
+  // chatRef.value.scrollBottom()
+})
+
+watch(
+  () => conversationList.value,
+  async (val) => {
+    if (!channelItem.value) {
+      return
+    }
+    const { list } = await getMoreMessages(1, channelItem.value)
+    if (list.value.length) {
+      const item = list.value[list.value.length - 1]
+      const arr = [101, 103, 104]
+      if (arr.includes(item.payload?.type)) {
+        getInterviewInviteList()
+      }
+    }
+    
+    items.value = list.value
+    setScrollBottom()
+    // 清除未读消息
+    resetUnread(channelItem.value)
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+
+async function init(userId, enterpriseId) {
+  const { channel, list, more } = await initChart(userId, enterpriseId)
+  hasMore.value = more
+  channelItem.value = channel.value
+  items.value = list.value
+  setScrollBottom()
+}
+
+
+const setScrollBottom = () => {
+  if (items.value?.length) {
+    const lastOne = items.value[items.value.length-1]
+    const index = items.value.length-1
+    newsId.value = 's' + lastOne.id + index // newsId 需要和聊天列表里面的id对应 永远取最后列表中的最后一个就可以做到发送消息即时滚动到底部
+  }
+}
+
+const hasWaitingProcessingInterview = ref(false) // 是否有待接受的面试邀请
+async function getInterviewInviteList () {
+  if (!info.value.id) return
+  const { data } = await getInterviewInviteListByInviteUserId(info.value.id)
+  interview.value = data ? data.slice(0, 1).map(e => {
+    const statusItem = statusList.value.find(_e => _e.value === e.status)
+    const statusText = statusItem?.label || ''
+    const statusColor = ['5', '98', '99'].includes(e.status)
+    return {
+      ...e,
+      statusColor: statusColor ? '#FE574A' : '#0E8E80',
+      statusText
+    }
+  }) : []
+  hasWaitingProcessingInterview.value = interview.value.some(e => e.status === '0')
+}
+
+const getStatusList = async () => {
+  try {
+    const { data } = await getDict('menduner_interview_invite_status')
+    if (data.data.length) {
+      statusList.value = data.data
+    }
+  } catch (error) {
+
+  }
+}
+
+function handleSend () {
+  if (!inputValue.value) {
+    uni.showToast({ title: '不能发送空白信息', icon: 'none' })
+    return
+  }
+  send(inputValue.value, channelItem.value)
+}
+
+function handleToCenter () {
+  uni.navigateTo({
+    url: '/pagesA/interview/index?index=1'
+  })
+}
+
+// 预览简历
+function handlePreview (payload) {
+  if (!payload?.content?.query?.src) {
+    uni.showToast({ title: '简历地址不存在', icon: 'none' })
+    return
+  }
+  preview(payload.content.query.src)
+}
+
+// 关闭职位列表窗口
+function  positionPopupClose () {
+  positionPopup.value.close()
+}
+
+// 打开职位列表窗口
+function positionPopupOpen () {
+  positionPopup.value.open('center')
+}
+
+// 选中职位并投递
+const selectJobId = ref('')
+const positionPopup = ref()
+const selectPositionSubmit = async () => {
+  if (!selectJobId.value) return uni.showToast({ title: '请选择要投递的职位', icon: 'none', duration: 3000 })
+  positionPopupClose()
+  handleFindResume() // 打开简历列表选择
+}
+
+const pageLoading = ref(false)
+const entPositionTotal = ref(0)
+const entPositionList = ref([])
+const entPositionListParams = ref({ pageNo: 1, pageSize: 5 })
+const entPositionListLastData = computed(() => entPositionListParams.value.pageNo * entPositionListParams.value.pageSize >=  entPositionTotal.value)
+// 职位列表
+const getRecruitPositionList = async () => {
+  const enterpriseId = info.value?.enterpriseId || null
+  if (!enterpriseId) return uni.showToast({ title: '访问企业错误!', icon: 'none', duration: 3000 })
+
+  pageLoading.value = true
+  const res = await getJobAdvertisedSearch({ ...entPositionListParams.value, enterpriseId })
+  const { list = [], total: number = 0 } = res?.data || {}
+  if (!list?.length) return uni.showToast({ title: '企业暂无招聘中的职位,无法进行投递!', icon: 'none', duration: 3000 })
+
+  entPositionTotal.value = number
+  entPositionList.value = list.map(j => {
+    const e = j?.job || null
+    if (!e) return e
+    const salary = e.payFrom && e.payTo ? `${e.payFrom ? e.payFrom + '-' : ''}${e.payTo}${e.payName ? '/' + e.payName : ''}` : '面议'
+    return {
+      label: `${formatName(e.name)}_${e.areaName ? e.area?.str : '全国'} ${salary}`,
+      value: e.id,
+      data: e
+    }
+  }).filter(Boolean)
+  
+  setTimeout(() => { pageLoading.value = false }, 300)
+}
+
+const changePositionData = () => {
+  entPositionListParams.value.pageNo = entPositionListLastData.value ? 1 : entPositionListParams.value.pageNo + 1
+  selectJobId.value = ''
+  getRecruitPositionList()
+}
+
+// 获取简历
+async function handleFindResume () {
+  if (isSendResume.value) {
+    return
+  }
+
+  // 没有基于职位接收到的沟通,弹出职位列表让求职者选择。否则无法投递简历。
+  if (!positionInfo.value.id && !selectJobId.value) {
+    await getRecruitPositionList()
+    if (entPositionTotal.value) positionPopupOpen()
+    return
+  }
+
+  try {
+    // 获取简历列表
+    const { data } = await getPersonResumeCv()
+    if (data.length === 0) {
+      uni.showToast({
+        title: '您还未上传过简历,请先上传简历后再投递',
+        icon: 'none',
+        mask: true,
+        duration: 3000
+      })
+    }
+    resumeList.value = data || []
+    resumeCheck.value = data && data.length ? data[0] : ''
+    popup.value.open('center')
+  } finally {
+    uni.hideLoading()
+  }
+}
+
+// 关闭简历窗口
+function handleClose () {
+  popup.value.close()
+}
+
+const studentDeliveryFormRef = ref()
+
+// 发送简历
+async function handleSendResume () {
+  if (!Object.keys(resumeCheck.value).length) {
+    uni.showToast({ title: '请选择要投递的简历', icon: 'none' })
+    return
+  }
+  let practice = null
+  if (isStudent.value) {
+    practice = await studentDeliveryFormRef.value.getQueryParams()
+    if (!practice) return
+  }
+
+  const text = {
+    remark: '发送简历',
+    query: {
+      src: resumeCheck.value.url,
+      title: resumeCheck.value.title,
+      id: resumeCheck.value.id,
+    },
+    type: 1
+  }
+  try {
+    if (isEmployment.value !== '-1') {
+      await jobCvRelHireSend({
+        jobId: positionInfo.value.id || selectJobId.value,
+        // ...(jobFairId && { jobFairId }),
+        url: resumeCheck.value.url,
+        recommendUserId: isEmployment.value,
+        ...(practice && { practice }),
+      })
+    } else {
+      await jobCvRelSend({
+        jobId: positionInfo.value.id || selectJobId.value,
+        // ...(jobFairId && { jobFairId }),
+        title: resumeCheck.value.title,
+        url: resumeCheck.value.url,
+        type: positionInfo.value.hire ? 1 : 0,
+        ...(practice && { practice }),
+      })
+    }
+    isSendResume.value = true
+    send (JSON.stringify(text), channelItem.value, 105)
+    popup.value.close()
+  } catch (error) {
+    if (error?.msg === '该职位已投递') {
+      isSendResume.value = true
+    }
+    popup.value.close()
+  }
+}
+
+// 拒绝邀请
+// function handleRefuse (val) {
+//   isAgree.value = false
+//   chooseInvite.value = val
+//   confirm.value.open()
+// }
+
+// 接受邀请
+// function handleAgree (val) {
+//   isAgree.value = true
+//   chooseInvite.value = val
+//   confirm.value.open()
+// }
+
+// 确认
+// async function handleConfirm () {
+//   // 拒绝
+//   if (!isAgree.value) {
+//     await userInterviewInviteReject(chooseInvite.value.id)
+//   } else {
+//     await userInterviewInviteReject(chooseInvite.value.id)
+//   }
+//   uni.showToast({ title: '操作成功', icon: 'none' })
+//   send(JSON.stringify({ id: chooseInvite.value.id }), channelItem.value, isAgree.value ? 104 : 103)
+// }
+
+// 关闭
+// function handleCloseConfirm () {
+//   confirm.value.close()
+// }
+
+// 查看更多
+async function handleMore () {
+  try {
+    uni.showLoading({
+      title: '加载中...'
+    })
+    pageSize.value++
+    const { list, more } = await getMoreMessages(pageSize.value, channelItem.value)
+    items.value.unshift(...list.value)
+    hasMore.value = more
+  } finally {
+    uni.hideLoading()
+  }
+}
+
+// 上传简历
+function handleUploadResume () {
+  wx.chooseMessageFile({
+    count: 1,
+    type: 'file',
+    success (res) {
+      // 限制文件上传大小
+      const size = res.tempFiles[0].size
+      if (size / (1024*1024) > 20) {
+        uni.showToast({ icon: 'none', title: '文件大小不能超过20M' })
+        return
+      }
+      
+      const title = res.tempFiles[0].name
+      const path = res.tempFiles[0].path
+      const test = /\.(pdf|docx|doc)$/.test(title)
+      if (!test) {
+        uni.showToast({
+          icon: 'none',
+          title: '请上传pdf、doc、docx类型的文件',
+          duration: 2000
+        })
+        return
+      }
+      //效验是否为支持的文件格式
+      uploadFile(path, 'attachment').then(async (res) => {
+        if (!res.data) {
+          uni.showToast({
+            title: '上传失败',
+            icon: 'none'
+          })
+          return
+        }
+        await saveResume({ title, url: res.data })
+        uni.showToast({
+          title: '上传成功',
+          icon: 'success'
+        })
+        handleFindResume()
+      })
+    }
+  })
+}
+
+// 获取职位信息
+async function getMessageTypeSync () {
+  try {
+    const { data } = await getMessageType({
+      fromUid: IM.uid,
+      channelId: channelItem.value.channelID,
+      type: 102,
+      page: {
+        current: 1,
+        size: 1,
+        orders: [
+          { column: 'message_seq', asc: false }
+        ]
+      }
+    })
+    if (!data.records || !data.records.length) { 
+      return
+    }
+    const _item = data.records.pop()
+    const _itemJSON = JSON.parse(_item.payload)
+    const _content = JSON.parse(_itemJSON.content)
+    positionInfo.value = _content.positionInfo
+    const { data: check } = await jobCvRelCheckSend({ jobId: _content.positionInfo.id })
+    isSendResume.value = check
+  } catch (error) {
+    console.log(345, error)
+  }
+}
+
+// let jobFairId = ''
+onLoad(async (options) => {
+  info.value = Object.keys(options).reduce((r, k) => {
+    r[k] = decodeURIComponent(options[k])
+    return r
+  }, {})
+  // jobFairId = info.value.jobFairId
+  isEmployment.value = info.value.isEmployment
+  channelItem.value = toChannel(info.value.channelID, info.value.channelType)
+
+  if (channelItem.value.channelID === 'system') {
+    const { list, more } = await getMoreMessages(1, channelItem.value)
+    hasMore.value = more
+    items.value = list.value
+    setScrollBottom()
+    // 清除未读消息
+    resetUnread(channelItem.value)
+    // 更新未读消息
+    updateUnreadCount()
+    return
+  }
+  await init(info.value.id, info.value.enterpriseId)
+  // 获取最新的职位信息
+  await getMessageTypeSync(info.value.id)
+
+  await getStatusList()
+  getInterviewInviteList()
+  // 清除未读消息
+  resetUnread(channelItem.value)
+  // 更新未读消息
+  updateUnreadCount()
+})
+</script>
+
+<style lang="scss" scoped>
+.white {
+  color: #FFF !important;
+}
+.text-left {
+  text-align: left !important;
+}
+.text-right {
+  text-align: right !important;
+}
+.box {
+  width: 100%;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  &-top {
+    &-title {
+      padding: 0 60rpx;
+      box-sizing: border-box;
+      width: 100%;
+      height: 80rpx;
+      line-height: 80rpx;
+      // text-align: center;
+      border-bottom: 2rpx solid #EEE;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      .subText {
+        font-size: .85em;
+        color: #999;
+        .gun {
+          padding: 0 10rpx;
+        }
+      }
+    }
+    &-content {
+      padding: 20rpx 50rpx;
+      padding-bottom: 20rpx;
+      border-bottom: 2rpx solid #eee;
+      .color-666 {
+        color: #666;
+      }
+      .font-weight-bold {
+        font-weight: bold;
+      }
+      .color-primary {
+        color: #009688;
+      }
+      .ml-3 {
+        margin-left: 40rpx;
+      }
+      .mt-1 {
+        margin-top: 12rpx;
+      }
+      .font-size-14 {
+        font-size: 24rpx;
+      }
+      .py-1 {
+        padding: 4rpx 0;
+      }
+      .tipsText {
+        font-size: .75em;
+        color: #999;
+      }
+      &-t {
+        display: flex;
+        justify-content: space-between;
+      }
+      .btnBox {
+        display: flex;
+        padding: 20rpx 60rpx;
+        justify-content: space-around;
+      }
+    }
+  }
+
+  &-main {
+    flex: 1;
+    height: 0;
+    padding: 40rpx;
+    // overflow-y: auto;
+    box-sizing: border-box;
+    &-more {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      color: #24bc3e;
+      font-size: .9em;
+      padding: 20rpx 0;
+    }
+    &-time {
+      user-select: none;
+      position: relative;
+      top: 16rpx;
+      margin: 40rpx 0;
+      text-align: center;
+      max-height: 40rpx;
+      text-align: center;
+      font-weight: 400;
+      font-size: .85em;
+      color: #999;
+    }
+    .jobCard {
+      padding: 30rpx;
+      background: #E2F2F0;
+      color: #009688 ;
+      margin-top: 20rpx;
+      max-width: unset;
+      margin-right: 0;
+      &-title {
+        font-size: 1.2em;
+      }
+      &-subtitle {
+        padding: 10rpx 0;
+        // font-size: .5em;
+      }
+      &-divider {
+        width: 100%;
+        height: 2rpx;
+        margin: 20rpx 0;
+        background: #ddd;
+      }
+      &-tag {
+        display: flex;
+        flex-wrap: wrap;
+      }
+    }
+    .message-view_item {
+      display: flex;
+      flex-direction: row;
+      align-items: flex-start;
+      margin: 16rpx 0;
+      position: relative;
+      .image {
+        width: 60rpx;
+        height: 60rpx;
+        border-radius: 180rpx;
+        // flex-grow: 1;
+        // flex-shrink: 0;
+        overflow: hidden;
+        .header {
+          width: 60rpx;
+          height: 60rpx;
+        }
+      }
+      .text-end {
+        text-align: right !important;
+        width: 400rpx;
+        margin-right: 20rpx;
+      }
+      .message-text {
+        overflow-wrap: break-word;
+        background-color: #f0f2f5;
+        border-radius: 12rpx;
+        max-width: 75%;
+        padding: 20rpx;
+        &.active {
+          background: #d5e6e8;
+        }
+        &.card {
+          background: #E2F2F0;
+          color: #009688 ;
+          margin-top: 20rpx;
+          max-width: unset;
+          margin-right: 0;
+          .btn-actions {
+            margin: 40rpx auto 20rpx auto ;
+            text-align: center;
+            .btn {
+              padding: 10rpx 30rpx;
+              background: #C8E7D8;
+              color: #43AC57;
+              font-size: .75em;
+              border-radius: 10rpx;
+            }
+          }
+        }
+        &.none {
+          padding: 10rpx 0;
+          background-color: unset;
+        }
+        &.active {
+          background: #d5e6e8;
+        }
+      }
+    }
+    .is-self {
+      flex-direction: row-reverse;
+      display: flex;
+      .message-text {
+        margin-right: 20rpx;
+      }
+    }
+    .is-other {
+      .message-text {
+        margin-left: 20rpx;
+      }
+    }
+  }
+  &-bottom {
+    max-height: 300rpx;
+    border-top: 2rpx solid #EEE;
+    background: rgba(230, 230, 230, 0.5);
+    padding: 20rpx 40rpx;
+    box-sizing: border-box;
+    &-tool {
+      margin-bottom: 30rpx;
+      .toolBtn {
+        padding: 12rpx 20rpx;
+        font-size: 24rpx;
+        background: #18bc37;
+        color: #FFF;
+        border-radius: 10rpx;
+      }
+    }
+    .textBox {
+      align-items: flex-end;
+    }
+    textarea {
+      border-radius: 10rpx;
+      width: 100%;
+      min-height: 80rpx;
+      max-height: 180rpx;
+      padding: 20rpx;
+      box-sizing: border-box;
+      background: #FFF;
+    }
+    .submitBtn {
+      width: 140rpx;
+      line-height: 80rpx;
+      height: 80rpx;
+      font-size: 28rpx;
+      background: #18bc37;
+      color: #FFF;
+      margin-left: 20rpx;
+      text-align: center;
+      border-radius: 10rpx;
+    }
+  }
+  .popup-title {
+    padding: 30rpx 20rpx;
+    display: flex;
+    justify-content: space-between;
+    border-bottom: 2rpx solid #DDD;
+  }
+  .popup-content {
+    padding: 20rpx 40rpx;
+    color: #999;
+    display: flex;
+    align-content: center;
+    justify-items: center;
+    .iconBox {
+      width: 40rpx;
+    }
+    .text {
+      margin-left: 20rpx;
+      &.active {
+        color: #00B760;
+      }
+    }
+  }
+  .popup-upload {
+    // display: flex;
+    // align-items: center;
+    // justify-content: center;
+    // flex-direction: column;
+    // width: 70%;
+    width: 80vw;
+    font-size: .75em;
+    color: #999;
+    padding: 40rpx;
+    &-box {
+      width: 200rpx;
+      height: 200rpx;
+      text-align: center;
+      line-height: 200rpx;
+      border: 4rpx solid #ddd;
+      border-radius: 8rpx;
+      margin: 0 auto;
+    }
+  }
+  .popup-actions {
+    padding: 60rpx;
+    .default {
+      background: #00B760;
+      color: #DDD;
+      font-size: .9em;
+    }
+  }
+}
+</style>

+ 8 - 16
pagesA/collect/company.vue

@@ -1,17 +1,15 @@
 <template>
-  <view class="defaultBgc">
+  <view class="ss-m-x-20">
     <scroll-view class="scrollBox" scroll-y="true" @scrolltolower="loadingMore" style="height: 100vh;">
       <view v-if="items.length">
-        <view v-for="(item, index) in items" :key="index" class="ss-m-t-20" @click="toDetail(item)">
-          <view style="background-color: #fff;" class="ss-p-30">
+        <view v-for="(item, index) in items" :key="index" class="ss-m-t-20" @click="jumpToEnterpriseDetail(item.id)">
+          <view style="background-color: #fff; border-radius: 12px;" class="ss-p-30">
             <view class="d-flex align-center">
-              <image :src="item.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'" class="avatar" style="width: 60px; height: 60px;"></image>
+              <image :src="item.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'" style="width: 50px; height: 50px; object-fit: contain"></image>
               <view style="flex: 1;" class="ss-m-l-30">
-                <view class="enterprise-name ellipsis">{{ item.name }}</view>
+                <view class="enterprise-name ellipsis">{{ formatName(item.anotherName || item.name) }}</view>
                 <view class="ss-m-y-15 font-size-12">
                   <span class="tag-gap color-666">
-                    <span>{{ item.financingName }}</span>
-                    <span class="ss-m-x-10" v-if="item.financingName && item.industryName">|</span>
                     <span>{{item.industryName }}</span>
                     <span class="ss-m-x-10" v-if="item.scaleName">|</span>
                     <span>{{item.scaleName }}</span>
@@ -44,7 +42,8 @@
 <script setup>
 import { ref } from 'vue'
 import { getSubscribeEnterprise } from '@/api/user'
-import { dealDictArrayData } from '@/utils/position'
+import { dealDictArrayData, jumpToEnterpriseDetail } from '@/utils/position'
+import { formatName } from '@/utils/getText'
 
 const items = ref([])
 const status = ref('more')
@@ -60,7 +59,7 @@ const getList = async () => {
     list = dealDictArrayData([], list)
     items.value = items.value.concat(list)
   }
-  status.value = list?.length < queryParams.value.pageSize ? 'noMore' : 'more'
+  status.value = items.value?.length === +data.total ? 'noMore' : 'more'
 }
 getList()
 
@@ -70,13 +69,6 @@ const loadingMore = () => {
   queryParams.value.pageNo++
   getList()
 }
-
-// 企业详情
-const toDetail = (item) => {
-  uni.navigateTo({
-    url: `/pagesB/companyDetail/index?id=${item.id}`
-  })
-}
 </script>
 
 <style scoped lang="scss">

+ 7 - 7
pagesA/collect/index.vue

@@ -1,8 +1,8 @@
 <template>
-  <view>
-    <uni-segmented-control :current="current" :values="controlList" @clickItem="handleChange" styleType="text" activeColor="#00897B"></uni-segmented-control>
-      <Position v-if="current === 0"></Position>
-      <Company v-else></Company>
+  <view class="defaultBgc">
+    <uni-segmented-control :current="current" :values="controlList" @clickItem="handleChange" styleType="text" activeColor="#00B760" style="background-color: #fff;"></uni-segmented-control>
+    <Position v-if="current === 0"></Position>
+    <Company v-else></Company>
   </view>
 </template>
 
@@ -17,10 +17,10 @@ const controlList = ['职位', '企业']
 const handleChange = (e) => {
   current.value = e.currentIndex
 }
-
-const loadingMore = () => {}
 </script>
 
 <style scoped lang="scss">
-
+:deep(.segmented-control) {
+  background-color: #fff !important;
+}
 </style>

+ 1 - 1
pagesA/collect/position.vue

@@ -35,7 +35,7 @@ const getList = async () => {
     })
     items.value = items.value.concat(list)
   }
-  status.value = list?.length < queryParams.value.pageSize ? 'noMore' : 'more'
+  status.value = items.value?.length === +data.total ? 'noMore' : 'more'
 }
 getList()
 

+ 198 - 0
pagesA/coupon/index.vue

@@ -0,0 +1,198 @@
+<template>
+  <layout-page>
+    <scroll-view class="scrollBox" scroll-y="true" @scrolltolower="loadingMore">
+      <view class="defaultBgc content">
+        <view
+          v-for="item in items"
+          :key="item.id"
+          class="content-item"
+          :class="{ used: item.status === 2, disabled: item.status === 3 }"
+        >
+          <view class="msg">
+            <view class="item">
+              <view class="name">{{ item.name }}</view>
+              <view class="price">
+                <uni-icons color="#f30" type="icon-renminbi1688" size="16" custom-prefix="iconfont"></uni-icons>
+                {{ item.price }}
+              </view>
+            </view>
+            <view class="item">
+              <view class="desc">有效期:{{ item.legalTime }}</view>
+              <view class="desc">满 {{ item.used }} 可用</view>
+            </view>
+          </view>
+          <view class="use">
+            <button
+            class="btn"
+            :class="{ disabled: item.status !== 1 }"
+            @tap="handleTo(item.status)"
+            >{{ item.status === 1 ? '立即使用' : item.status === 2 ? '已使用' : '已过期'}}</button>
+          </view>
+        </view>
+        <uni-load-more :status="more" />
+      </view>
+    </scroll-view>
+  </layout-page>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import {
+  getCouponPage
+} from '@/api/sign'
+import { timesTampChange } from '@/utils/date'
+
+
+const pageInfo = ref({
+  pageNo:1,
+  pageSize: 20
+})
+const total = ref(0)
+const items = ref([])
+const more = ref('more')
+
+getMyCoupon()
+
+async function getMyCoupon () {
+  try {
+    const { data } = await getCouponPage({ ...pageInfo.value })
+    if (!data || !data.list || !data.list.length) {
+      if (pageInfo.value.pageNo === 1) {
+        more.value = 'more'
+        return
+      }
+      pageInfo.value.pageNo--
+      more.value = 'more'
+      return
+    }
+    items.value.push(...data.list.map(e => {
+      return {
+        ...e,
+        price: (e.discountPrice / 100).toFixed(2),
+        legalTime: timesTampChange(e.validStartTime, 'Y-M-D') + ' 至 ' + timesTampChange(e.validEndTime, 'Y-M-D'),
+        used: (e.usePrice / 100).toFixed(2)
+      }
+    }))
+    total.value = +data.total
+    more.value = items.value.length >= total.value ? 'noMore' : 'more'
+  } catch (error) {
+    if (pageInfo.value.pageNo === 1) {
+      more.value = 'more'
+      return
+    }
+    pageInfo.value.pageNo--
+    more.value = 'more'
+  }
+}
+
+function handleTo (status) {
+  if (status !== 1) {
+    return
+  }
+  wx.navigateToMiniProgram({  
+    appId: 'wx6decdf12f9e7a061', // 目标小程序的 appId
+    // envVersion: 'develop',
+    success(res) {  
+        // 打开成功  
+        console.log('成功跳转至小程序:', res);  
+    },  
+    fail(err) {  
+        // 打开失败
+        uni.showToast({
+          title: '打开商城失败',
+          icon: 'none'
+        })
+    }  
+  })
+}
+
+function loadingMore () {
+  if (more.value === 'noMore') {
+    return
+  }
+  more.value = 'loading'
+  pageInfo.value.pageNo++
+  getMyCoupon()
+}
+</script>
+
+<style lang="scss" scoped>
+.content {
+  // height: 100vh;
+  width: 100vw;
+  padding: 20rpx;
+  box-sizing: border-box;
+  &-item {
+    margin-bottom: 20rpx;
+    .msg {
+      background: #FFF;
+      padding: 40rpx;
+      box-sizing: border-box;
+      -webkit-mask: radial-gradient(circle at 0.375rem 100%, #00000000 0.375rem, red 0) -0.375rem;
+      position: relative;
+      &::after {
+        content: '';
+        position: absolute;
+        bottom: 0;
+        right: 0.375rem;
+        width: calc( 100% - 0.75rem);
+        height: 0;
+        border-top: 2rpx dashed #eee;
+      }
+    }
+    .use {
+      background: #FFF;
+      padding: 20rpx;
+      box-sizing: border-box;
+      display: flex;
+      justify-content: flex-end;
+      font-size: .85em ;
+      -webkit-mask: radial-gradient(circle at 0.375rem 0%, #0000 0.375rem, red 0) -0.375rem;
+      .btn {
+        margin: 0;
+        position: relative;
+        border: 0;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        box-sizing: border-box;
+        text-align: center;
+        text-decoration: none;
+        white-space: nowrap;
+        vertical-align: baseline;
+        transform: translate(0, 0);
+        padding: 0 0.5rem;
+        height: 1.5625rem;
+        width: 200rpx;
+        border-radius: 1.25rem;
+        background: linear-gradient(90deg, #ff3000, rgba(255, 48, 0, 0.6));
+        color: #ffffff;
+        font-size: 0.75rem;
+        font-weight: 400;
+        &.disabled {
+          background: rgba(130, 130, 130, 0.5);
+        }
+      }
+    }
+    .item {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      margin-bottom: 20rpx;
+      .name {
+        font-weight: bolder;
+        font-size: 36rpx;
+      }
+      .price {
+        color: #f30;
+        font-size: 54rpx;
+      }
+      .desc {
+        color: #999;
+        font-size: 24rpx;
+      }
+      
+    }
+  }
+}
+</style>

+ 34 - 32
pagesA/info/index.vue

@@ -3,9 +3,9 @@
 		<uni-forms ref="form" :modelValue="formData" :rules="rules" validateTrigger="bind" label-width="105px" label-align="right">
 			<uni-forms-item label="头像" name="avatar" class="f-straight">
         <view style="display: flex;flex-wrap: wrap;">
-          <view class="upload-img" v-if="formData.avatar" @click="handlePreviewImage">
-            <uni-icons size="30" type="clear" color="#00897B" style="position: absolute;right: -11px; top: -11px; z-index: 9" @click="formData.avatar = ''"></uni-icons>
-            <image :src="formData.avatar" mode="contain" style="width: 200rpx;height: 200rpx;"></image>
+          <view class="upload-img" v-if="formData?.avatar" @click="handlePreviewImage">
+            <uni-icons size="30" type="clear" color="#fe574a" style="position: absolute;right: -11px; top: -11px; z-index: 9" @click="formData.avatar = ''"></uni-icons>
+            <image :src="formData?.avatar" mode="contain" style="width: 200rpx;height: 200rpx;"></image>
           </view>
           <view v-else class="upload-file" @click="uploadPhotos">
             <uni-icons type="plusempty" size="50" color="#f1f1f1"></uni-icons>
@@ -18,16 +18,16 @@
       <uni-forms-item label="性别" name="sex" required>
 				<uni-data-checkbox v-model="formData.sex" :localdata="sexData" />
 			</uni-forms-item>
-      <uni-forms-item label="电话号码" name="phone">
+      <uni-forms-item label="联系电话" name="phone">
         <uni-easyinput v-model="formData.phone" placeholder="请输入电话号码" />
 			</uni-forms-item>
-      <uni-forms-item required label="常用邮箱" name="email">
+      <uni-forms-item label="常用邮箱" name="email" required>
         <uni-easyinput v-model="formData.email" placeholder="请输入常用邮箱" />
 			</uni-forms-item>
       <uni-forms-item required label="出生日期" name="birthday">
         <uni-datetime-picker type="date" return-type="timestamp" v-model="formData.birthday" />
 			</uni-forms-item>
-      <uni-forms-item required label="首次工作时间" name="firstWorkTime">
+      <uni-forms-item label="首次工作时间" name="firstWorkTime">
         <uni-datetime-picker type="date" return-type="timestamp" v-model="formData.firstWorkTime" />
 			</uni-forms-item>
 			<uni-forms-item required label="工作年限" name="expType" >
@@ -42,13 +42,13 @@
       <uni-forms-item required label="求职状态" name="jobStatus" >
 				<uni-data-picker v-model="formData.jobStatus" :localdata="dictObj.jobStatus" :clear-icon="false" popup-title="请选择求职状态" :map="map"></uni-data-picker>
 			</uni-forms-item>
-      <uni-forms-item required label="婚姻状况" name="maritalStatus" >
+      <uni-forms-item label="婚姻状况" name="maritalStatus" >
 				<uni-data-picker v-model="formData.maritalStatus" :localdata="dictObj.marital" :clear-icon="false" popup-title="请选择婚姻状况" :map="map"></uni-data-picker>
 			</uni-forms-item>
-      <uni-forms-item required label="所在城市" name="areaId" >
+      <uni-forms-item label="所在城市" name="areaId" >
 				<uni-data-picker v-model="formData.areaId" :localdata="dictObj.areaTreeData" :clear-icon="false" popup-title="请选择所在城市" :map="{ text: 'name', value: 'id'}"></uni-data-picker>
 			</uni-forms-item>
-      <uni-forms-item required label="户籍地" name="regId" >
+      <uni-forms-item label="户籍地" name="regId" >
 				<uni-data-picker v-model="formData.regId" :localdata="dictObj.areaTreeData" :clear-icon="false" popup-title="请选择户籍所在地" :map="{ text: 'name', value: 'id'}"></uni-data-picker>
 			</uni-forms-item>
 			<view class="f-horizon-center">
@@ -65,14 +65,31 @@ import { dictObj } from '@/utils/position.js'
 import { uploadFile } from '@/api/file'
 import { cloneDeep } from 'lodash-es'
 import { saveBaseInfo, updatePersonAvatar } from '@/api/user'
+import { emailRequired } from '@/utils/validate'
 
 const form = ref()
 const sexData = ref([])
 const map = { text: 'label', value: 'value' }
 const useUserStore = userStore()
+
 const formData = ref({})
 const getInfo = () => {
-  formData.value = cloneDeep(useUserStore?.baseInfo)
+  formData.value = cloneDeep(useUserStore?.baseInfo) || {
+    avatar: '',
+    name: '',
+    sex: '',
+    regId: '',
+    birthday: '',
+    phone: '',
+    email: '',
+    eduType: '',
+    firstWorkTime: '',
+    expType: '',
+    jobType: '',
+    jobStatus: '',
+    maritalStatus: '',
+    areaId: ''
+  }
 }
 getInfo()
 
@@ -98,7 +115,7 @@ const uploadPhotos = () => {
     sourceType: ['album', 'camera'],
     success: function(res){
       const path = res.tempFilePaths[0]
-      uploadFile(path).then(res => {
+      uploadFile(path, 'img').then(res => {
         formData.value.avatar = res.data
       }).catch(error => {
         uni.showToast({
@@ -118,15 +135,10 @@ const rules = {
 	sex : {
 		rules: [{required: true, errorMessage: '请选择您的性别' }]
 	},
-	email: {
-		rules: [{required: true, errorMessage: '请输入您的常用邮箱' }]
-	},
 	birthday: {
 		rules: [{required: true, errorMessage: '请选择您的出生日期' }]
 	},
-  firstWorkTime: {
-		rules: [{required: true, errorMessage: '请选择您的首次工作时间' }]
-	},
+  email: emailRequired,
   expType: {
 		rules: [{required: true, errorMessage: '请选择您的工作年限' }]
 	},
@@ -138,28 +150,18 @@ const rules = {
 	},
   jobStatus: {
 		rules: [{required: true, errorMessage: '请选择您的求职状态' }]
-	},
-  maritalStatus: {
-		rules: [{required: true, errorMessage: '请选择您的婚姻状况' }]
-	},
-  areaId: {
-		rules: [{required: true, errorMessage: '请选择您的所在城市' }]
-	},
-  regId: {
-		rules: [{required: true, errorMessage: '请选择您的户籍所在地' }]
 	}
 }
 
 const submit = async () => {
   const valid = await unref(form).validate()
   if (!valid) return
-  console.log(formData.value.avatar, formData.value)
 
-  // await updatePersonAvatar(formData.value.avatar)
-  // await saveBaseInfo(formData.value)
-  // uni.showToast({ title: '编辑成功', icon: 'success' })
-  // useUserStore.getInfo()
-  // getInfo()
+  await updatePersonAvatar(formData.value.avatar)
+  await saveBaseInfo(formData.value)
+  uni.showToast({ title: '编辑成功', icon: 'success' })
+  await useUserStore.getInfo()
+  getInfo()
 }
 </script>
 

+ 175 - 0
pagesA/integral/index.vue

@@ -0,0 +1,175 @@
+<template>
+  <layout-page>
+    <view class="box defaultBgc">
+      <scroll-view class="scrollBox" scroll-y="true" @scrolltolower="loadingMore">
+        <view>
+          <view class="panel">
+            <view>
+              <!-- <uni-icons color="#f30" type="icon-renminbi1688" size="16" custom-prefix="iconfont"></uni-icons> -->
+              <text class="text">{{ balance.point }}</text>
+            </view>
+            <view>
+              <button class="btn" @tap="handleUse">积分兑换</button>
+            </view>
+          </view>
+          <view class="list">
+            <uni-list>
+              <uni-list-item
+                v-for="item in items"
+                :key="item.id"
+                :title="item.description"
+                :rightText="item._createTime"
+              />
+            </uni-list>
+            <uni-load-more :status="more" />
+          </view>
+        </view>
+      </scroll-view>
+
+      <uni-popup ref="inputDialog" type="dialog">
+        <view class="shareQrCodePopupContent">
+          <view class="ss-m-b-10">请前往网页版门墩儿商城查看</view>
+          <uni-link href="https://www.menduner.com/mall" text="点击复制网页地址" color="#00B760" fontSize="16" copyTips="已复制,请在电脑端打开"></uni-link>
+        </view>
+      </uni-popup>
+    </view>
+  </layout-page>
+</template>
+<!-- balance 余额 -->
+<script setup>
+import { ref } from 'vue'
+import {
+  getUserAccount,
+  getEnterpriseAccountRecordPage
+} from '@/api/sign'
+import { timesTampChange } from '@/utils/date'
+
+const balance = ref({})
+
+const items = ref([])
+const pageInfo = ref({
+  pageNo: 1,
+  pageSize: 20
+})
+const total = ref(0)
+const more = ref('more')
+const inputDialog = ref()
+
+getBalance()
+getList()
+// 获取积分余额
+async function getBalance() {
+  const { data } = await getUserAccount()
+  if (!data) {
+    return
+  }
+  balance.value = data
+}
+
+async function getList () {
+  try {
+    const { data } = await getEnterpriseAccountRecordPage({ ...pageInfo.value, type: 0 })
+    if (!data || !data.list) {
+      if (pageInfo.value.pageNo === 1) {
+        return
+      }
+      pageInfo.value.pageNo--
+      more.value = 'more'
+      return
+    }
+    const _data = data.list.map(e => {
+      return {
+        ...e,
+        _createTime: timesTampChange(e.createTime)
+      }
+    })
+    items.value.push(..._data)
+    total.value = +data.total
+    more.value = total.value <= items.value.length ? 'noMore' : 'more'
+  } catch (error) {
+    if (pageInfo.value.pageNo === 1) {
+      return
+    }
+    pageInfo.value.pageNo--
+    more.value = 'more'
+  }
+}
+function loadingMore () {
+  if (more.value === 'noMore') {
+    return
+  }
+  more.value = 'loading'
+  pageInfo.value.pageNo++
+  getList()
+}
+
+function handleUse () {
+  // wx.navigateToMiniProgram({  
+  //   appId: 'wx6decdf12f9e7a061', // 目标小程序的 appId
+  //   // envVersion: 'develop',
+  //   success(res) {  
+  //       // 打开成功  
+  //       console.log('成功跳转至小程序:', res);  
+  //   },  
+  //   fail(err) {  
+  //       // 打开失败
+  //       uni.showToast({
+  //         title: '打开商城失败',
+  //         icon: 'none'
+  //       })
+  //   }  
+  // })
+  inputDialog.value.open()
+}
+
+</script>
+
+<style lang="scss" scoped>
+.box {
+  height: 100vh;
+}
+.scrollBox {
+  width: 100vw;
+  // padding: 20rpx;
+  box-sizing: border-box;
+  .panel {
+    position: sticky;
+    z-index: 2;
+    top: 0;
+    width: 100%;
+    padding: 20rpx;
+    margin-bottom: 20rpx;
+    box-sizing: border-box;
+    background: #FFF;
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-end;
+    box-shadow: 0px 0px 10px 0px rgb(0, 0, 0, 0.25);
+    .text {
+      color: #F30;
+      font-size: 54rpx;
+
+    }
+    .btn {
+      width: 180rpx;
+      height: 60rpx;
+      line-height: 60rpx;
+      font-size: 28rpx;
+      text-align: center;
+      background: #00B760;
+      color: #FFF;
+      border-radius: 30rpx;
+    }
+  }
+  .list {
+    // padding: 20rpx;
+  }
+}
+.shareQrCodePopupContent {
+	width: 75vw;
+	padding: 40rpx;
+	margin-bottom: 20rpx;
+	text-align: center;
+	background-color: #fff;
+}
+</style>

+ 11 - 3
pagesA/interview/index.vue

@@ -1,6 +1,6 @@
 <template>
   <view>
-    <uni-segmented-control :current="current" :values="controlList" @clickItem="handleChange" styleType="text" activeColor="#00897B"></uni-segmented-control>
+    <uni-segmented-control :current="current" :values="controlList" @clickItem="handleChange" styleType="text" activeColor="#00B760"></uni-segmented-control>
     <scroll-view class="scrollBox defaultBgc" scroll-y="true" @scrolltolower="loadingMore" style="height: calc(100vh - 36px);">
       <view v-if="items.length">
         <PositionList v-if="current === 0" class="pb-10" :list="items" :noMore="false"></PositionList>
@@ -28,6 +28,7 @@ import { dealDictObjData } from '@/utils/position'
 import PositionList from '@/components/PositionList'
 import Items from './item.vue'
 import { userStore } from '@/store/user'
+import { onLoad } from '@dcloudio/uni-app'
 
 const useUserStore = userStore()
 const current = ref(0)
@@ -44,6 +45,14 @@ const popup = ref()
 const type = ref('')
 const id = ref(null)
 
+onLoad((options) => {
+  if (options?.index) {
+    current.value = Number(options.index)
+    items.value = []
+  }
+  getList()
+})
+
 const getList = async () => {
   const api = current.value === 0 ? getJobDeliveryList : getUserInterviewInvitePage
   if (current.value !== 0) queryParams.value.status = statusList[current.value - 1]
@@ -60,9 +69,8 @@ const getList = async () => {
     })
     items.value = items.value.concat(list)
   }
-  more.value = list?.length < queryParams.value.pageSize ? 'noMore' : 'more'
+  more.value = items.value?.length === +data.total ? 'noMore' : 'more'
 }
-getList()
 
 const handleChange = (e) => {
   items.value = []

+ 17 - 12
pagesA/interview/item.vue

@@ -1,12 +1,12 @@
 <template>
-  <view v-if="list.length > 0">
+  <view v-if="list.length > 0" class="ss-m-x-20">
     <view v-for="(item, index) in list" :key="index">
-      <view class="sub-li-bottom" @click.stop="toDetail(0, item)">
+      <view class="sub-li-bottom" @click.stop="jumpToEnterpriseDetail(item.enterprise?.id)">
         <view class="avatarBox">
           <image class="enterAvatar" :src="item.enterprise?.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'"></image>
         </view>
         <view>
-          <span class="ss-m-x-20" style="font-weight: bold;">{{ item.contact?.name || ' -- ' }}</span>
+          <span class="ss-m-x-20 color-66" style="font-weight: bold;">{{ item.contact?.name || ' -- ' }}</span>
           <span>{{ item.contact?.postNameCn }}</span>
           <span class="divider tag-gap1 ss-m-x-10" v-if="item.contact?.postNameCn && item.invitePhone"> | </span>
           <span class="mr">{{ item.invitePhone }}</span>
@@ -14,12 +14,13 @@
       </view>
       <!-- 职位信息 -->
       <view class="list-shape">
-        <view class="titleBox my-5" @click="toDetail(1, item)">
-          <span style="font-size: 16px;font-weight: 700;color: black;">{{item.job?.name}}</span>
-          <span class="salary-text">{{ item.job?.payFrom }}-{{ item.job?.payTo }}/{{ item.job?.payName }}</span>
+        <view class="titleBox my-5" @click="toDetail(item)">
+          <span style="font-size: 16px;font-weight: 700; color: #0E100F;">{{ formatName(item.job?.name) }}</span>
+          <span v-if="!item.job?.payFrom && !item.job?.payTo" class="salary-text">面议</span>
+          <span v-else class="salary-text">{{ item.job?.payFrom }}-{{ item.job?.payTo }}{{ item.job?.payName ? '/' + item.job?.payName : '' }}</span>
         </view>
         <!-- 面试时间、地点 -->
-        <view class="color-666 font-size-14 ss-m-t-20" @click="toDetail(1, item)">
+        <view class="color-666 font-size-14 ss-m-t-20" @click="toDetail(item)">
           <view>面试时间:{{ timesTampChange(item.time, 'Y-M-D h:m') }}</view>
           <view class="ss-m-t-20">面试地点:{{ item.address }}</view>
         </view>
@@ -28,7 +29,7 @@
 					<view class="divided-line"></view>
 					<view class="d-flex justify-end">
 						<span style="color: #dd524d;text-decoration: underline;" @click="handleAction(item, 'refuse')">拒绝</span>
-						<span style="color: #00897B;margin-left: 65rpx;text-decoration: underline;" @click="handleAction(item, 'agree')">同意</span>
+						<span style="color: #00B760;margin-left: 65rpx;text-decoration: underline;" @click="handleAction(item, 'agree')">同意</span>
 					</view>
 				</view>
       </view>
@@ -39,6 +40,8 @@
 
 <script setup>
 import { timesTampChange } from '@/utils/date'
+import { formatName } from '@/utils/getText'
+import { jumpToEnterpriseDetail } from '@/utils/position'
 
 const emits = defineEmits(['action'])
 const props = defineProps({
@@ -46,9 +49,8 @@ const props = defineProps({
 })
 
 //岗位详情
-const toDetail = (isPosition, item) =>{
-  const url = isPosition ? `/pagesB/positionDetail/index?id=${item.job?.id}` : `/pagesB/companyDetail/index?id=${item.enterprise?.id}`
-  uni.navigateTo({ url })
+const toDetail = (item) =>{
+  uni.navigateTo({ url: `/pagesB/positionDetail/index?id=${item.job?.id}&area=${item.job.area?.str ?? '全国'}` })
 }
 
 const handleAction = (item, type) => {
@@ -89,6 +91,7 @@ const handleAction = (item, type) => {
   background: linear-gradient(90deg, #f5fcfc 0, #fcfbfa 100%);
   font-size: 13px;
   padding: 5px 30rpx;
+  border-radius: 12px 12px 0 0;
   .avatarBox {
     max-width: 40px;
     max-height: 40px;
@@ -97,11 +100,13 @@ const handleAction = (item, type) => {
 
 .salary-text {
 	float: right;
-	color: #fe574a;
+	color: #00B760;
+  font-weight: 700;
 }
 .list-shape {
 	padding: 10px 30rpx 10px;
   background-color: #fff;
+  border-radius: 0 0 12px 12px;
   .titleBox {
     display: flex;
     align-items: center;

+ 123 - 0
pagesA/recommendation/index.vue

@@ -0,0 +1,123 @@
+<template>
+	<layout-page>
+		<uni-segmented-control :current="current" :values="controlListText" @clickItem="handleChange" styleType="text" activeColor="#00B760"></uni-segmented-control>
+		<scroll-view class="scrollBox defaultBgc" scroll-y="true" @scrolltolower="loadingMore" style="height: calc(100vh - 72rpx);">
+		  <view v-if="items.length" class="listBox">
+		    <m-list :items="items"></m-list>
+		    <uni-load-more :status="more" />
+		  </view>
+		  <view v-else class="nodata-img-parent">
+		    <image
+					src="https://minio.citupro.com/dev/static/nodata.png"
+					mode="widthFix"
+					style="width: 100vw"
+				></image>
+		  </view>
+		</scroll-view>
+	</layout-page>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+import layoutPage from '@/layout'
+
+import MList from './list'
+import { getDict } from '@/hooks/useDictionaries.js'
+import { getRecommendationList } from '@/api/position.js'
+import { onLoad } from '@dcloudio/uni-app'
+
+import { userStore } from '@/store/user'
+const useUserStore = userStore()
+
+watch(() => useUserStore.refreshToken, (newVal, oldVal) => {
+  if (useUserStore.refreshToken) {
+		// 监听登录状态
+		console.log('重新登录了')
+		handleChange({ currentIndex: current.value })
+	}
+})
+
+
+const current = ref(0)
+
+// 获取参数
+const controlList = ref([])
+const controlListText = ref([])
+const pageInfo = ref({
+	pageNo: 1,
+	pageSize: 10
+})
+const total = ref(0)
+const items = ref([])
+const loading = ref(false)
+const more = ref('more')
+
+async function initDict () {
+	try {
+		const { data } = await getDict('menduner_hire_job_cv_status')
+		if (!data?.data) {
+			return
+		}
+		controlList.value = data.data
+		controlListText.value = data.data.map(e => e.label)
+		// current.value = +controlList.value[0].value
+		init()
+	} catch (error) {
+		// console.log(error)
+	}
+}
+
+
+function handleChange (val) {
+	current.value = val.currentIndex
+	pageInfo.value.pageNo = 1
+	total.value = 0
+	items.value = []
+	init()
+}
+
+function loadingMore () {
+	if (total.value === items.value.length) {
+		return
+	}
+	if (loading.value) {
+		return
+	}
+	more.value = 'loading'
+  pageInfo.value.pageNo++
+	init()
+}
+
+async function init () {
+	try {
+		loading.value = true
+		const { data } = await getRecommendationList({
+			...pageInfo.value,
+			status: current.value
+		})
+		if (!data?.list) {
+			pageInfo.value.pageNo--
+			return
+		}
+		items.value.push(...data.list)
+		total.value = +data.total
+		more.value = items.value.length === total.value ? 'noMore' : 'more'
+	} catch (error) {
+		pageInfo.value.pageNo--
+	} finally {
+		loading.value = false
+	}
+}
+
+onLoad(async (options) => {
+  if (options?.id) {
+    current.value = +options.id
+	}
+	initDict()
+})
+
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 70 - 0
pagesA/recommendation/list.vue

@@ -0,0 +1,70 @@
+<template>
+	<view>
+		<view
+			class="list ss-m-x-15"
+			v-for="item in items"
+			:key="item.id"
+		>
+			<view class="list-top">
+				<text class="list-top-person">牛人:{{ item.sendPerson?.name }}</text>
+				<text class="list-top-time">{{ timesTampChange(item.createTime) }}</text>
+			</view>
+			<view class="list-remuneration">
+				薪酬:
+				<span v-if="item.job?.payFrom && item.job?.payTo">{{ item.job?.payFrom + '-' + item.job?.payTo }}</span>
+				<span v-else>面议</span>
+			</view>
+			<view class="list-company" style="border-radius: 0 0 12px 12px;">
+				<text>{{ formatName(item.enterprise?.anotherName || item.enterprise?.name) }}</text>
+				<text>{{ item.enterprise?.anotherName && item.job?.name ? ' · ' : '' }}</text>
+				<text>{{ formatName(item.job?.name) }}</text>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+import { timesTampChange } from '@/utils/date'
+import { formatName } from '@/utils/getText'
+
+const props = defineProps({
+	items: {
+		type: Array,
+		default: () => []
+	}
+})
+</script>
+
+<style lang="scss" scoped>
+.list {
+	background: #fff;
+	margin-top: 20rpx;
+	border-radius: 12px;
+	&-top {
+		padding: 20rpx;
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		&-person {
+			font-size: .9em;
+			color: #333;
+		}
+		&-time {
+			font-size: .75em;
+			color: #999;
+		}
+	}
+	&-company {
+		padding: 30rpx 20rpx;
+		font-size: 28rpx;
+		color: #666;
+		background: linear-gradient(90deg, #f5fcfc 0, #fcfbfa 100%);
+	}
+	&-remuneration {
+		padding: 20rpx;
+		font-size: 28rpx;
+		color: #666;
+	}
+	
+}
+</style>

+ 47 - 10
pagesA/resume/index.vue

@@ -1,24 +1,29 @@
 <template>
   <view class="ss-p-b-100" style="height: 100vh; background-color: #f2f4f7;">
-    <uni-notice-bar show-close single text="最多可以上传5份附件简历" />
-    <view v-if="bioList.length > 0" class="ss-p-t-6">
-			<uni-card v-for="(item, index) in bioList" :key="index" :is-shadow="true" :border='false' shadow="0px 0px 3px 1px rgba(0,0,0,0.1)">
-				<view class="d-flex align-center">
+    <uni-notice-bar text="温馨提示:最多可以上传5份附件简历。请在手机上打开此小程序进行文件上传,暂不支持在桌面版小程序中上传文件。" />
+    <view v-if="bioList?.length > 0">
+      <uni-card v-for="(item, index) in bioList" :key="index" :is-shadow="true" :border='false' shadow="0px 0px 3px 1px rgba(0,0,0,0.1)">
+        <view class="d-flex align-center">
+          <view v-if="props.resumeAnalysis" class="ss-m-r-15">
+            <radio :value="index" color="#00B760" :checked="index === checkedIndex" @tap="radioChange(index)" />
+          </view>
           <view @click="preview(item.url)"  style="flex: 1;">
             <view class="font-size-14" style="font-weight: bold;">{{ item.title }}</view>
-				    <view>上传时间:{{ timesTampChange(item.createTime, 'Y-M-D') }}</view>
+            <view>上传时间:{{ timesTampChange(item.createTime, 'Y-M-D') }}</view>
           </view>
           <view class="ss-m-l-30" style="width: 60rpx;">
             <uni-icons @click="handleOpenPopup(item)" type="more-filled" size="20"></uni-icons>
           </view>
         </view>
-			</uni-card>
+      </uni-card>
 		</view>
 		<view v-else class="nodata-img-parent">
 			<image src="https://minio.citupro.com/dev/static/nodata.png" mode="widthFix" style="width: 100vw;height: 100vh;"></image>
 		</view>
-    <view class="bottom-sticky">
-      <button class="recomm-button" @click="handleUpload">微信聊天文件上传</button>
+    <view class="bottom-sticky flex-column">
+      <button v-if="props.resumeAnalysis" class="recomm-button" style="margin-bottom: 0;" :loading="analysisLoading" @click="handleResumeAnalysis">开始解析</button>
+      <button class="recomm-button" style="margin-bottom: 0;" @click="handleUpload">微信聊天文件上传</button>
+      <view class="color-primary font-size-14 ss-m-b-25 ss-m-t-10" style="text-align: center;">上传文件大小不能超过20MB</view>
     </view>
 
     <uni-popup ref="popup" type="bottom">
@@ -35,6 +40,11 @@ import { uploadFile } from '@/api/file'
 import { timesTampChange } from '@/utils/date'
 import { preview } from '@/utils/preview'
 
+const emit = defineEmits(['submit'])
+const props = defineProps({
+  resumeAnalysis: { type: Boolean, default: false }
+})
+
 // 获取附件
 const bioList = ref([])
 const getList = async () => {
@@ -78,11 +88,18 @@ const handleUpload = () => {
     count: 1,
     type: 'file',
     success (res) {
+      // 限制文件上传大小
+      const size = res.tempFiles[0].size
+      if (size / (1024*1024) > 20) {
+        uni.showToast({ icon: 'none', title: '文件大小不能超过20M' })
+        return
+      }
+
       const title = res.tempFiles[0].name
       const path = res.tempFiles[0].path
       //效验是否为支持的文件格式
       if(/\.(pdf|docx|doc)$/.test(title)){
-        uploadFile(path).then(async (res) => {
+        uploadFile(path, 'attachment').then(async (res) => {
           if (!res.data) {
             uni.showToast({
               title: '上传失败',
@@ -100,7 +117,7 @@ const handleUpload = () => {
       }else{
         uni.showToast({
           icon: 'none',
-          title: '请上传pdf、word类型的文件',
+          title: '请上传pdf、doc、docx类型的文件',
           duration: 2000
         })
         return
@@ -108,6 +125,26 @@ const handleUpload = () => {
     }
   })
 }
+
+const fileUrl = ref('')
+const checkedIndex = ref()
+const radioChange = (index) => {
+  if (!props.resumeAnalysis) return
+  checkedIndex.value = index
+  if (bioList.value[index]?.url) fileUrl.value = encodeURIComponent(bioList.value[index].url)
+}
+
+const analysisLoading = ref(false)
+const handleResumeAnalysis = () => {
+  if (!fileUrl.value) return uni.showToast({ icon: 'none', title: '请选择要解析的简历' })
+  analysisLoading.value = true
+  emit('submit', fileUrl.value)
+	// uni.navigateTo({
+	// 	url: `/pagesA/resumeAnalysis/index?fileUrl=${fileUrl.value}`
+	// })
+}
+
+
 </script>
 
 <style scoped lang="scss">

+ 93 - 0
pagesA/resumeAnalysis/components/advantage.vue

@@ -0,0 +1,93 @@
+<template>
+	<view class="f-straight wrapper">
+		<uni-forms ref="form" :modelValue="formData" :rules="rules" validateTrigger="bind" label-width="105px" label-align="right">
+      <uni-forms-item label="个人优势" name="advantage" required>
+        <textarea
+          placeholder-style="color:#F76260"
+          placeholder="请填写您的个人优势..."
+          auto-focus
+          maxlength="300"
+          v-model="formData.advantage"
+          style="border: 1rpx solid gray; width: 100%; min-height: 300px;"
+        ></textarea>
+			</uni-forms-item>
+		</uni-forms>
+	</view>
+</template>
+
+<script setup>
+import { ref, watch, unref } from 'vue'
+const props = defineProps({
+  id: {
+    type: String,
+    default: ''
+  },
+  text: {
+    type: String,
+    default: ''
+  },
+  data: {
+    type: String,
+    default: ''
+  }
+})
+
+const form = ref()
+const formData = ref({ advantage:'' })
+
+watch(
+  () => props.data,
+  (newVal) => {
+    formData.value.advantage = newVal
+  },
+  { immediate: true },
+)
+
+
+const rules = {
+	advantage:{
+		rules: [{required: true, errorMessage: '请上传头像' }]
+	}
+}
+
+const submit = async () => {
+  try {
+    const valid = await unref(form).validate()
+    // if (!valid) return { id: props.id, data: null}
+    return { id: props.id, data: { content: formData.value.advantage }}
+  } catch (error) {
+    return { text: props.text }
+  }
+}
+
+defineExpose({
+  id: props.id,
+  submit
+})
+
+</script>
+
+<style lang="less" scoped>
+
+.wrapper{
+	padding: 15px;
+  padding-top: 30px;
+}
+.upload-img{
+  position: relative;
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+}
+.upload-file{
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 10rpx;
+}
+</style>

+ 119 - 0
pagesA/resumeAnalysis/components/avatarEdit.vue

@@ -0,0 +1,119 @@
+<template>
+	<view class="f-straight wrapper">
+		<uni-forms ref="form" :modelValue="formData" :rules="rules" validateTrigger="bind" label-width="105px" label-align="right">
+			<uni-forms-item label="头像" name="avatar" class="f-straight" required>
+        <view style="display: flex;flex-wrap: wrap;">
+          <view class="upload-img" v-if="formData?.avatar">
+            <uni-icons size="35" type="clear" color="#fe574a" style="position: absolute;right: -15px; top: -15px; z-index: 9" @click="formData.avatar = ''"></uni-icons>
+            <image :src="formData?.avatar" mode="contain" style="width: 200rpx;height: 200rpx;" @click="handlePreviewImage"></image>
+          </view>
+          <view v-else class="upload-file" @click="uploadPhotos">
+            <uni-icons type="plusempty" size="50" color="#f1f1f1"></uni-icons>
+          </view>
+        </view>
+			</uni-forms-item>
+		</uni-forms>
+	</view>
+</template>
+
+<script setup>
+import { pathToBase64 } from '@/utils/image-tools.js'
+import { ref, watch } from 'vue'
+const props = defineProps({
+  id: {
+    type: String,
+    default: ''
+  },
+  text: {
+    type: String,
+    default: ''
+  },
+  data: {
+    type: String,
+    default: ''
+  }
+})
+
+const form = ref()
+const formData = ref({ avatar:'' })
+
+watch(
+  () => props.data,
+  (newVal) => {
+    formData.value.avatar = newVal
+  },
+  { immediate: true },
+)
+
+// 图片预览
+const handlePreviewImage = () => {
+  uni.previewImage({
+    current: 0,
+    urls: [formData.value.avatar]
+  })
+}
+
+// 选择头像
+const uploadPhotos = () => {
+  wx.chooseImage({
+    count: 1,
+    sizeType: ['original', 'compressed'],
+    sourceType: ['album', 'camera'],
+    async success(res) {
+      console.log('res:', res)
+      const size = res.tempFiles[0]?.size || 0
+      if (size >= 31457280) {
+        uni.showToast({
+          icon: 'none',
+          title: '头像上传大小不得超过 20MB !',
+          duration: 2000
+        })
+        return
+      }
+      // 选取图片并转为base64
+      formData.value.avatar = await pathToBase64(res.tempFilePaths[0])
+    }
+  })
+}
+
+const rules = {
+	avatar:{
+		rules: [{required: true, errorMessage: '请上传头像' }]
+	}
+}
+
+const submit = async () => {
+  return { id: props.id, data: formData.value.avatar}
+}
+
+defineExpose({
+  id: props.id,
+  submit
+})
+
+</script>
+
+<style lang="less" scoped>
+
+.wrapper{
+	padding: 15px;
+  padding-top: 30px;
+}
+.upload-img{
+  position: relative;
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+}
+.upload-file{
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 10rpx;
+}
+</style>

+ 180 - 0
pagesA/resumeAnalysis/components/baseInfoEdit.vue

@@ -0,0 +1,180 @@
+<template>
+	<view class="f-straight wrapper">
+		<uni-forms ref="form" :modelValue="formData" :rules="rules" validateTrigger="bind" label-width="105px" label-align="right">
+			<uni-forms-item required label="中文名" name="name">
+        <uni-easyinput v-model="formData.name" placeholder="请输入中文名" />
+			</uni-forms-item>
+      <uni-forms-item label="性别" name="sex" required>
+				<uni-data-checkbox v-model="formData.sex" :localdata="sexData" />
+			</uni-forms-item>
+      <uni-forms-item label="联系电话" name="phone" clearable>
+        <uni-easyinput v-model="formData.phone" placeholder="请输入电话号码" />
+			</uni-forms-item>
+      <uni-forms-item label="常用邮箱" name="email" clearable required>
+        <uni-easyinput v-model="formData.email" placeholder="请输入常用邮箱" />
+			</uni-forms-item>
+      <uni-forms-item required label="出生日期" name="birthday">
+        <uni-datetime-picker type="date" return-type="timestamp" v-model="formData.birthday" />
+			</uni-forms-item>
+      <uni-forms-item label="首次工作时间" name="firstWorkTime">
+        <uni-datetime-picker type="date" return-type="timestamp" v-model="formData.firstWorkTime" />
+			</uni-forms-item>
+			<uni-forms-item required label="工作年限" name="expType" >
+				<uni-data-picker v-model="formData.expType" :localdata="dictObj.exp" :clear-icon="false" popup-title="请选择工作年限" :clear="false" :map="map"></uni-data-picker>
+			</uni-forms-item>
+      <uni-forms-item required label="最高学历" name="eduType" >
+				<uni-data-picker v-model="formData.eduType" :localdata="dictObj.edu" :clear-icon="false" popup-title="请选择最高学历" :clear="false" :map="map"></uni-data-picker>
+			</uni-forms-item>
+			<uni-forms-item required label="求职类型" name="jobType" >
+				<uni-data-picker v-model="formData.jobType" :localdata="dictObj.jobType" :clear-icon="false" popup-title="请选择求职类型" :map="map"></uni-data-picker>
+			</uni-forms-item>
+      <uni-forms-item required label="求职状态" name="jobStatus" >
+				<uni-data-picker v-model="formData.jobStatus" :localdata="dictObj.jobStatus" :clear-icon="false" popup-title="请选择求职状态" :map="map"></uni-data-picker>
+			</uni-forms-item>
+      <uni-forms-item label="婚姻状况" name="maritalStatus" >
+				<uni-data-picker v-model="formData.maritalStatus" :localdata="dictObj.marital" :clear-icon="true" popup-title="请选择婚姻状况" :map="map"></uni-data-picker>
+			</uni-forms-item>
+      <uni-forms-item label="所在城市" name="areaId" >
+				<uni-data-picker v-model="formData.areaId" :localdata="dictObj.areaTreeData" :clear-icon="true" popup-title="请选择所在城市" :map="{ text: 'name', value: 'id'}"></uni-data-picker>
+			</uni-forms-item>
+      <uni-forms-item label="户籍地" name="regId" >
+				<uni-data-picker v-model="formData.regId" :localdata="dictObj.areaTreeData" :clear-icon="true" popup-title="请选择户籍所在地" :map="{ text: 'name', value: 'id'}"></uni-data-picker>
+			</uni-forms-item>
+		</uni-forms>
+	</view>
+</template>
+
+<script setup>
+import { cloneDeep } from 'lodash-es'
+import { emailRequired } from '@/utils/validate'
+import { dictObj } from '@/utils/position.js'
+import { userStore } from '@/store/user'
+import { removeEmptyProperties } from "@/utils/index"
+// import { saveBaseInfo, updatePersonAvatar } from '@/api/user'
+// import { uploadFile } from '@/api/file'
+import { ref, watch, unref } from 'vue'
+const props = defineProps({
+  id: {
+    type: String,
+    default: ''
+  },
+  text: {
+    type: String,
+    default: ''
+  },
+  data: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+const form = ref()
+const sexData = ref([])
+const map = { text: 'label', value: 'value' }
+const useUserStore = userStore()
+
+const formData = ref({
+  name: '',
+  sex: '',
+  regId: '',
+  birthday: '1985-05-01',
+  phone: '',
+  email: '',
+  eduType: '',
+  firstWorkTime: '',
+  expType: '',
+  jobType: '',
+  jobStatus: '',
+  maritalStatus: '',
+  areaId: ''
+})
+const getInfo = (data) => {
+  data.phone = data.phone || useUserStore?.baseInfo?.phone
+  Object.keys(formData.value).forEach(key => formData.value[key] = data[key])
+}
+
+watch(
+  () => props.data,
+  (newVal) => {
+    if (newVal && Object.keys(newVal)) {
+      const data = removeEmptyProperties(newVal)
+      getInfo(data)
+    }
+  },
+  { immediate: true },
+)
+
+if (dictObj && dictObj?.sex) {
+  sexData.value = dictObj.sex.map(e => {
+    return { text: e.label, value: e.value, ...e }
+  })
+}
+
+const rules = {
+	name:{
+		rules: [{required: true, errorMessage: '请输入姓名' }]
+	},
+	sex : {
+		rules: [{required: true, errorMessage: '请选择您的性别' }]
+	},
+	birthday: {
+		rules: [{required: true, errorMessage: '请选择您的出生日期' }]
+	},
+  email: emailRequired,
+  expType: {
+		rules: [{required: true, errorMessage: '请选择您的工作年限' }]
+	},
+  eduType: {
+		rules: [{required: true, errorMessage: '请选择您的最高学历' }]
+	},
+  jobType: {
+		rules: [{required: true, errorMessage: '请选择您的求职类型' }]
+	},
+  jobStatus: {
+		rules: [{required: true, errorMessage: '请选择您的求职状态' }]
+	}
+}
+
+
+const submit = async () => {
+  try {
+    const valid = await unref(form).validate()
+    // if (!valid) return { id: props.id, data: null}
+    return { id: props.id, data: formData.value}
+  } catch (error) {
+    return { text: props.text }
+  }
+}
+
+defineExpose({
+  id: props.id,
+  submit
+})
+
+
+</script>
+
+<style lang="less" scoped>
+
+.wrapper{
+	padding: 15px;
+  // padding-top: 30px;
+}
+.upload-img{
+  position: relative;
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+}
+.upload-file{
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 10rpx;
+}
+</style>

+ 177 - 0
pagesA/resumeAnalysis/components/educationExp.vue

@@ -0,0 +1,177 @@
+<template>
+	<view class="f-straight wrapper">
+    <uni-forms ref="form" :modelValue="formData" :rules="rules" validateTrigger="bind" label-width="90px">
+      <uni-forms-item label="学校名称" name="schoolName" required>
+				<uni-combox :candidates="schoolData" placeholder="学校名称" v-model="formData.schoolName" @input="handleSearchSchool"></uni-combox>
+			</uni-forms-item>
+      <uni-forms-item label="所学专业" name="major" required>
+				<uni-combox :candidates="majorData" placeholder="所学专业" v-model="formData.major" @input="handleSearchMajor"></uni-combox>
+			</uni-forms-item>
+      <uni-forms-item label="学历" name="educationType" required>
+        <uni-data-select v-model="formData.educationType" :localdata="searchData.eduType"></uni-data-select>
+			</uni-forms-item>
+      <uni-forms-item label="学制类型" name="educationSystemType" required>
+        <uni-data-select v-model="formData.educationSystemType" :localdata="searchData.eduSystemType"></uni-data-select>
+			</uni-forms-item>
+      <uni-forms-item label="开始时间" name="startTime" required>
+				<picker mode="date" :value="formData.startTime" fields="month" :end="endDate" @change="e => formData.startTime = e.detail.value">
+					<view class="uni-input ss-m-t-20">{{ formData.startTime }}</view>
+				</picker>
+			</uni-forms-item>
+      <uni-forms-item label="结束时间" name="endTime" required>
+				<picker mode="date" :value="formData.endTime" fields="month" @change="e => formData.endTime = e.detail.value">
+					<view class="uni-input ss-m-t-20">{{ formData.endTime }}</view>
+				</picker>
+			</uni-forms-item>
+      <uni-forms-item label="在校经历" name="content">
+				<uni-easyinput type="textarea" v-model="formData.content" autoHeight  placeholder="请输入内容"></uni-easyinput>
+			</uni-forms-item>
+    </uni-forms>
+	</view>
+</template>
+
+<script setup>
+import { cloneDeep } from 'lodash-es'
+import { ref, watch, unref } from 'vue'
+import { dictObj } from '@/utils/position.js'
+import { convertYearMonthToTimestamp, timesTampChange } from '@/utils/date.js'
+import { removeEmptyProperties } from "@/utils/index"
+const props = defineProps({
+  id: {
+    type: String,
+    default: ''
+  },
+  text: {
+    type: String,
+    default: ''
+  },
+  data: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+let formData = ref({
+  startTime: '2014-01',
+  endTime: '2018-01'
+})
+const majorData = ref([])
+const schoolData = ref([])
+const form = ref()
+const date = new Date()
+const endDate = date.getFullYear() + '-' + (date.getMonth() + 1) // 不可选时间
+const searchData = ref({
+  school: [],
+  major: [],
+  eduType: dictObj.edu.map(e => ({ text: e.label, value: e.value})),
+  eduSystemType: dictObj.eduSystemType.map(e => ({ text: e.label, value: e.value}))
+})
+
+const getInfo = (data) => {
+  data.startTime = data.startTime ? timesTampChange(data.startTime, 'Y-M') : '2014-01'
+  data.endTime = data.endTime ? timesTampChange(data.endTime, 'Y-M') : '2018-01'
+  formData.value = cloneDeep(data) || {
+    startTime: '2014-01',
+    endTime: '2018-01'
+  }
+}
+
+watch(
+  () => props.data,
+  (newVal) => {
+    if (newVal && Object.keys(newVal)) {
+      const data = removeEmptyProperties(newVal)
+      getInfo(data)
+    }
+  },
+  { immediate: true },
+)
+
+const rules = {
+	schoolName:{
+		rules: [{required: true, errorMessage: '请输入学校名称' }]
+	},
+	major:{
+		rules: [{required: true, errorMessage: '请输入所学专业' }]
+	},
+	educationType:{
+		rules: [{required: true, errorMessage: '请选择学历' }]
+	},
+	educationSystemType:{
+		rules: [{required: true, errorMessage: '请选择学制类型' }]
+	},
+	startTime:{
+		rules: [{required: true, errorMessage: '请选择开始时间' }]
+	},
+	endTime:{
+		rules: [{required: true, errorMessage: '请选择结束时间' }]
+	}
+}
+
+
+// 学校搜索
+const handleSearchSchool = (e) => {
+  if (!e) return schoolData.value = []
+  schoolSearchByName({ name: e }).then(res => {
+    searchData.value.school = res.data
+    schoolData.value = res.data && res.data?.length ? res.data.map(e => e.value) : []
+  })
+}
+
+// 专业搜索
+const handleSearchMajor = (e) => {
+  if (!e) return majorData.value = []
+  schoolMajorByName({ name: e }).then(res => {
+    searchData.value.major = res.data
+    majorData.value = res.data && res.data?.length ? res.data.map(e => e.nameCn) : []
+  })
+}
+
+const submit = async () => {
+  try {
+    const valid = await unref(form).validate()
+    // if (!valid) return { id: props.id, data: null}
+    const startTime = convertYearMonthToTimestamp(formData.value.startTime)
+    const endTime = convertYearMonthToTimestamp(formData.value.endTime)
+    if (startTime > endTime) {
+      uni.showToast({ icon: 'none', title: '开始时间不能大于结束时间' })
+      return
+    }
+    return { id: props.id, data: { ...formData.value, startTime, endTime }}
+  } catch (error) {
+    return { text: props.text }
+  }
+}
+
+defineExpose({
+  id: props.id,
+  submit
+})
+
+
+</script>
+
+<style lang="less" scoped>
+
+.wrapper{
+	padding: 15px;
+  // padding-top: 30px;
+}
+.upload-img{
+  position: relative;
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+}
+.upload-file{
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 10rpx;
+}
+</style>

+ 137 - 0
pagesA/resumeAnalysis/components/trainingExperience.vue

@@ -0,0 +1,137 @@
+<template>
+	<view class="f-straight wrapper">
+    <uni-forms ref="form" :modelValue="formData" :rules="rules" validateTrigger="bind" label-width="90px">
+      <uni-forms-item label="培训中心" name="orgName" required>
+				<uni-easyinput type="text" v-model="formData.orgName" placeholder="请输入内容"></uni-easyinput>
+			</uni-forms-item>
+      <uni-forms-item label="培训课程" name="course" required>
+				<uni-easyinput type="text" v-model="formData.course" placeholder="请输入内容"></uni-easyinput>
+			</uni-forms-item>
+      <uni-forms-item label="开始时间" name="startTime" required>
+				<picker mode="date" :value="formData.startTime" fields="month" :end="endDate" @change="e => formData.startTime = e.detail.value">
+					<view class="uni-input ss-m-t-20">{{ formData.startTime }}</view>
+				</picker>
+			</uni-forms-item>
+      <uni-forms-item label="结束时间" name="endTime" required>
+				<picker mode="date" :value="formData.endTime" fields="month" :end="endDate" @change="e => formData.endTime = e.detail.value">
+          <view class="uni-input ss-m-t-20">{{ formData.endTime }}</view>
+        </picker>
+			</uni-forms-item>
+      <uni-forms-item label="培训描述" name="content">
+				<uni-easyinput type="textarea" v-model="formData.content" autoHeight  placeholder="请输入内容"></uni-easyinput>
+			</uni-forms-item>
+    </uni-forms>
+	</view>
+</template>
+
+<script setup>
+import { cloneDeep } from 'lodash-es'
+import { ref, watch, unref } from 'vue'
+import { convertYearMonthToTimestamp, timesTampChange } from '@/utils/date.js'
+import { removeEmptyProperties } from "@/utils/index"
+const props = defineProps({
+  id: {
+    type: String,
+    default: ''
+  },
+  text: {
+    type: String,
+    default: ''
+  },
+  data: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+let formData = ref({
+  startTime: '2014-01',
+  endTime: '2018-01'
+})
+const form = ref()
+const date = new Date()
+const endDate = date.getFullYear() + '-' + (date.getMonth() + 1) // 不可选时间
+
+const getInfo = (data) => {
+  data.startTime = data.startTime ? timesTampChange(data.startTime, 'Y-M') : '2014-01'
+  data.endTime = data.endTime ? timesTampChange(data.endTime, 'Y-M') : '2018-01'
+  formData.value = cloneDeep(data) || {
+    startTime: '2014-01',
+    endTime: '2018-01'
+  }
+}
+
+watch(
+  () => props.data,
+  (newVal) => {
+    if (newVal && Object.keys(newVal)) {
+      const data = removeEmptyProperties(newVal)
+      getInfo(data)
+    }
+  },
+  { immediate: true },
+)
+
+const rules = {
+	orgName:{
+		rules: [{required: true, errorMessage: '请输入培训中心' }]
+	},
+	course:{
+		rules: [{required: true, errorMessage: '请输入培训课程' }]
+	},
+	startTime:{
+		rules: [{required: true, errorMessage: '请选择培训开始时间' }]
+	},
+  endTime:{
+		rules: [{required: true, errorMessage: '请选择培训结束时间' }]
+	}
+}
+
+const submit = async () => {
+  try {
+    const valid = await unref(form).validate()
+    // if (!valid) return { id: props.id, data: null}
+    const startTime = convertYearMonthToTimestamp(formData.value.startTime)
+    const endTime = convertYearMonthToTimestamp(formData.value.endTime)
+    if (startTime > endTime) {
+      uni.showToast({ icon: 'none', title: '开始时间不能大于结束时间' })
+      return
+    }
+    return { id: props.id, data: { ...formData.value, startTime, endTime }}
+  } catch (error) {
+    return { text: props.text }
+  }
+}
+
+defineExpose({
+  id: props.id,
+  submit
+})
+
+
+</script>
+
+<style lang="less" scoped>
+
+.wrapper{
+	padding: 15px;
+  // padding-top: 30px;
+}
+.upload-img{
+  position: relative;
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+}
+.upload-file{
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 10rpx;
+}
+</style>

+ 182 - 0
pagesA/resumeAnalysis/components/workExperience.vue

@@ -0,0 +1,182 @@
+<template>
+	<view class="f-straight wrapper">
+    <uni-forms ref="form" :modelValue="formData" :rules="rules" validateTrigger="bind" label-width="90px">
+      <uni-forms-item label="企业名称" name="enterpriseName" required>
+				<uni-combox :candidates="enterpriseData" placeholder="企业名称" v-model="formData.enterpriseName" @input="handleSearchEnterprise"></uni-combox>
+			</uni-forms-item>
+      <uni-forms-item label="职位名称" name="positionName" required>
+				<uni-combox :candidates="positionData" placeholder="职位名称" v-model="formData.positionName"></uni-combox>
+			</uni-forms-item>
+      <uni-forms-item label="开始时间" name="startTime" required>
+				<picker mode="date" :value="formData.startTime" fields="month" :end="endDate" @change="e => formData.startTime = e.detail.value">
+					<view class="uni-input ss-m-t-20">{{ formData.startTime }}</view>
+				</picker>
+			</uni-forms-item>
+      <uni-forms-item label="结束时间" name="endTime" required>
+				<view class="d-flex">
+          <picker mode="date" :value="formData.endTime" :disabled="endDisabled" fields="month" :end="endDate" @change="e => formData.endTime = e.detail.value">
+            <view class="uni-input ss-m-t-20" :style="{'opacity': endDisabled ? '0.5' : '1'}">{{ formData.endTime }}</view>
+          </picker>
+          <uni-data-checkbox selectedColor="#00B760" class="ss-m-l-50 ss-m-t-14" multiple v-model="sofar" :localdata="[{ text: '至今', value: 1 }]" @change="handleChangeSofar"></uni-data-checkbox>
+        </view>
+			</uni-forms-item>
+      <uni-forms-item label="工作内容" name="content" required>
+				<uni-easyinput type="textarea" v-model="formData.content" autoHeight  placeholder="请输入内容"></uni-easyinput>
+			</uni-forms-item>
+    </uni-forms>
+	</view>
+</template>
+
+<script setup>
+import { cloneDeep } from 'lodash-es'
+import { ref, watch, unref } from 'vue'
+import { getDict } from '@/hooks/useDictionaries.js'
+import { convertYearMonthToTimestamp, timesTampChange } from '@/utils/date.js'
+import { removeEmptyProperties } from "@/utils/index"
+const props = defineProps({
+  id: {
+    type: String,
+    default: ''
+  },
+  text: {
+    type: String,
+    default: ''
+  },
+  data: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+let formData = ref({
+  startTime: '2014-01',
+  endTime: '2018-01'
+})
+const sofar = ref([])
+const endDisabled = ref(false)
+const enterpriseData = ref([])
+const positionData = ref([])
+const form = ref()
+const date = new Date()
+const endDate = date.getFullYear() + '-' + (date.getMonth() + 1) // 不可选时间
+const searchData = ref({
+  enterprise: [],
+  position: []
+})
+
+const getInfo = (data) => {
+  data.startTime = data.startTime ? timesTampChange(data.startTime, 'Y-M') : '2014-01'
+  data.endTime = data.endTime ? timesTampChange(data.endTime, 'Y-M') : null
+  if (!data.endTime) {
+    endDisabled.value = true
+    data.endTime = '2018-01'
+    sofar.value = [1]
+  }
+  formData.value = cloneDeep(data) || {
+    startTime: '2014-01',
+    endTime: '2018-01'
+  }
+}
+
+watch(
+  () => props.data,
+  (newVal) => {
+    if (newVal && Object.keys(newVal)) {
+      const data = removeEmptyProperties(newVal)
+      getInfo(data)
+    }
+  },
+  { immediate: true },
+)
+
+const rules = {
+	enterpriseName:{
+		rules: [{required: true, errorMessage: '请输入企业名称' }]
+	},
+	positionName:{
+		rules: [{required: true, errorMessage: '请输入职位名称' }]
+	},
+	startTime:{
+		rules: [{required: true, errorMessage: '请选择开始时间' }]
+	},
+  content:{
+		rules: [{required: true, errorMessage: '请输入工作内容' }]
+	}
+}
+
+
+// 企业搜索
+const handleSearchEnterprise = (e) => {
+  if (!e) return enterpriseData.value = []
+  enterpriseSearchByName({ name: e }).then(res => {
+    searchData.value.enterprise = res.data
+    enterpriseData.value = res.data && res.data?.length ? res.data.map(e => e.value) : []
+  })
+}
+
+let positionTreeChildrenData = []
+getDict('positionTreeData', null, 'positionTreeData').then(({ data }) => {
+  data = data.data?.length && data.data || []
+  data.forEach(e => {
+    if (e?.children?.length) positionTreeChildrenData = positionTreeChildrenData.concat(e.children)
+  })
+  searchData.value.position = positionTreeChildrenData
+  positionData.value = positionTreeChildrenData.map(e => e.nameCn)
+})
+
+// 至今
+const handleChangeSofar = (e) => {
+  const value = e.detail.value.length ? e.detail.value[0] : ''
+  endDisabled.value = value ? true : false
+}
+
+const submit = async () => {
+  try {
+    const valid = await unref(form).validate()
+    // if (!valid) return { id: props.id, data: null}
+    if (!formData.value.endTime && !sofar.value.length) return uni.showToast({ icon: 'none', title: '请选择工作经历的结束时间' })
+    //
+    const startTime = convertYearMonthToTimestamp(formData.value.startTime)
+    const endTime = sofar.value.length ? null : convertYearMonthToTimestamp(formData.value.endTime)
+    if (endTime && startTime > endTime) {
+      uni.showToast({ icon: 'none', title: '开始时间不能大于结束时间' })
+      return
+    }
+    return { id: props.id, data: { ...formData.value, startTime, endTime }}
+  } catch (error) {
+    return { text: props.text }
+  }
+}
+
+defineExpose({
+  id: props.id,
+  submit
+})
+
+
+</script>
+
+<style lang="less" scoped>
+
+.wrapper{
+	padding: 15px;
+  // padding-top: 30px;
+}
+.upload-img{
+  position: relative;
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+}
+.upload-file{
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 10rpx;
+}
+</style>

+ 221 - 0
pagesA/resumeAnalysis/index.vue

@@ -0,0 +1,221 @@
+<!-- 简历附件解析 -->
+<template>
+  <view>
+    <!-- 选择简历 -->
+    <resume v-if="step === 1" resumeAnalysis @submit="handleResumeAnalysis"></resume>
+    <!-- 解析内容-表单 -->
+    <view v-if="step === 2 && formLIst?.length" style="padding-bottom: 150rpx;">
+      <uni-card v-for="item of formLIst" :key="item.id" :id="item.id">
+		    <uni-section :title="item.text" type="line">
+          <template v-slot:right>
+            <view v-if="item.path !== 'baseInfoEdit'" style="color: #e64340;" @click="del(item)">删除</view>
+          </template>
+          <avatarEdit v-if="item.path === 'avatarEdit'" ref="componentRef" :id="item.id"  :text="item.text" :data="item.data" />
+          <baseInfoEdit v-if="item.path === 'baseInfoEdit'" ref="componentRef" :id="item.id"  :text="item.text" :data="item.data" />
+          <advantageEdit v-if="item.path === 'advantageEdit'" ref="componentRef" :id="item.id"  :text="item.text" :data="item.data" />
+          <educationEdit v-if="item.path === 'educationEdit'" ref="componentRef" :id="item.id"  :text="item.text" :data="item.data" />
+          <workExperienceEdit v-if="item.path === 'workExperienceEdit'" ref="componentRef" :id="item.id"  :text="item.text" :data="item.data" />
+          <trainingExperienceEdit v-if="item.path === 'trainingExperienceEdit'" ref="componentRef" :id="item.id"  :text="item.text" :data="item.data" />
+        </uni-section>
+      </uni-card>
+      <!-- 保存 -->
+      <view class="bottom-sticky flex-column ss-p-b-25" style="background-color: #fff; z-index: 2000; border-top: 1px solid #eee;">
+        <button class="recomm-button" :loading="submitLoading" @click="submit">提交(保存至在线简历)</button>
+      </view>
+    </view>
+    <view v-if="step === 3">
+      <view class="tips">加载中...</view>
+    </view>
+    <view v-if="step === 4">
+      <view class="tips">加载失败</view>
+    </view>
+    <!-- 确认框 -->
+    <uni-popup ref="confirmRef" type="dialog">
+      <uni-popup-dialog
+        type="warn"
+        cancelText="取消"
+        confirmText="确认" 
+        title="系统提示"
+        :showClose="showClose"
+        :content="dialogContent"
+        @confirm="handleConfirm"
+        @close="null"
+      ></uni-popup-dialog>
+    </uni-popup>
+  </view>
+</template>
+
+<script setup>
+import { saveResumeInfo, resumeParser2 } from '@/api/user'
+// import { envObj, baseUrl } from '@/utils/config'
+import resume from '../resume/index.vue'
+import { ref, shallowRef } from 'vue'
+import avatarEdit from './components/avatarEdit.vue'
+import baseInfoEdit from './components/baseInfoEdit.vue'
+import advantageEdit from './components/advantage.vue'
+import educationEdit from './components/educationExp.vue'
+import workExperienceEdit from './components/workExperience.vue'
+import trainingExperienceEdit from './components/trainingExperience.vue'
+import { resumeParser2Data } from './testData.js'
+
+const step = ref(1)
+
+const exampleList = {
+  avatar: { text: '头像', id: 'avatar', path: 'avatarEdit' },
+  person: { text: '基础信息', id: 'person', path: 'baseInfoEdit' },
+  advantage: { text: '个人优势', id: 'advantage', path: 'advantageEdit' },
+  eduList: { text: '教育经历', id: 'eduList', path: 'educationEdit' },
+  workList: { text: '工作经历', id: 'workList', path: 'workExperienceEdit' },
+  trainList: { text: '培训经历', id: 'trainList', path: 'trainingExperienceEdit' },
+}
+
+const resumeTxt = ref([]) // 查看文本信息
+const formLIst = shallowRef([])
+const transformToLIst = async (result) => {
+  formLIst.value = []
+  if (result && Object.keys(result)) {
+    if (result.resume?.rawText) resumeTxt.value = result.resume.rawText.split('\n') || []
+    if (result.person?.advantage) result.advantage = result.person.advantage
+    if (result.person?.avatar) result.avatar = result.person.avatar
+    // obj
+    const dealObjKeys = ['avatar', 'person', 'advantage']
+    dealObjKeys.forEach(key => {
+      if (result[key]) {
+        const obj = {...exampleList[key]}
+        obj.data = result[key]
+        formLIst.value.push(obj)
+      }
+    })
+    // arr
+    const dealArrKeys = ['eduList', 'workList', 'trainList']
+    dealArrKeys.forEach(key => {
+      if (result[key]?.length) {
+        for (let index = 0; index < result[key].length; index++) {
+          const obj = {...exampleList[key]}
+          obj.id = obj.id + '_' + index
+          obj.text = result[key].length > 1 ? obj.text + (index+1) : obj.text
+          obj.data = result[key][index]
+          formLIst.value.push(obj)
+        }
+      }
+    })
+  }
+  // console.log('formLIst:', formLIst.value)
+}
+
+const confirmRef = ref()
+const dialogContent = ref('')
+const showClose = ref(true)
+let delId = null
+let dialogType = 'del'
+
+const del = (item) => {
+  dialogContent.value = `是否确认删除${item.text}?`
+  delId = item.id
+  dialogType = 'del'
+  showClose.value = true
+  confirmRef.value.open()
+}
+
+const handleConfirm = () => {
+  if (dialogType === 'del') {
+    formLIst.value = formLIst.value.filter(e => e.id !== delId)
+  }
+  if (dialogType === 'submitSuccess') {
+    uni.navigateTo({ url: '/pagesA/resumeOnline/index' })
+  }
+}
+
+// const result = ref(JSON.parse(JSON.stringify(data))) // 测试
+// transformToLIst(result.value) // 测试
+
+const result = ref({})
+const loading = ref(false)
+const handleAnalysis = async (url) => {
+  url = decodeURIComponent(url)
+  if (!url) return
+  loading.value = true
+  step.value = 3
+  try {
+    // const res = await resumeParser2({ fileUrl: url })
+    // result.value = res?.data || {}
+    result.value = resumeParser2Data
+    await transformToLIst(result.value)
+    step.value = 2
+  } catch (error) {
+    step.value = 4
+    console.log(error)
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleResumeAnalysis = (url) => {
+  if (!url) {
+    return uni.showToast({ icon: 'none', title: '请选择要解析的简历' })
+  }
+  handleAnalysis(url)
+}
+
+const componentRef = ref()
+const getValue = async () => {
+  let text = ''
+  let data = {}
+  for (let index = 0; index < componentRef.value.length; index++) {
+    const e = componentRef.value[index]
+    const query = await e.submit()
+    if (query && query.data) {
+      data[query.id] = query.data
+    } else {
+      if (!text) text = query.text
+    }
+  }
+  if (text) {
+    uni.showToast({ icon: 'none', title: `请完整填写 ${text} 后提交!` })
+    return
+  }
+  // 处理data
+  let obj = Object.keys(data).length ? {} : null
+  const keyTransform = { // 转换给后端的key
+    eduList: 'eduExp',
+    workList: 'workExp',
+    trainList: 'trainExp',
+  }
+  if (obj) {
+    Object.keys(data).forEach(key => {
+      if (key.includes('_')) { // 数组
+        const oldKey = key.split('_')[0]
+        const newKey = keyTransform[oldKey] ? keyTransform[oldKey] : oldKey
+        if (!obj[newKey]) obj[newKey] = [data[key]]
+        else obj[newKey].push(data[key])
+      } else {
+        const newKey = keyTransform[key] ? keyTransform[key] : key
+        obj[newKey] = data[key]
+      }
+    })
+  }
+  console.log('123456:', obj)
+  return obj && Object.keys(obj).length ? JSON.stringify(obj) : null
+}
+
+const submitLoading = ref(false)
+const submit = async () => {
+  const obj = await getValue()
+  if (!obj) return
+  submitLoading.value = true
+  await saveResumeInfo(obj)
+  dialogType = 'submitSuccess'
+  dialogContent.value = '提交成功,立即前往在线简历查看'
+  showClose.value = false
+  confirmRef.value.open()
+  // await useUserStore().getUserBaseInfos() // 更新用户信息
+}
+
+</script>
+<style lang="scss" scoped>
+.tips {
+  text-align: center;
+  margin-top: 50px;
+  color: #777;
+}
+</style>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 9 - 0
pagesA/resumeAnalysis/testData.js


+ 50 - 0
pagesA/resumeOnline/advantage.vue

@@ -0,0 +1,50 @@
+<!--  -->
+<template>
+  <view style="padding: 20rpx 30rpx;">
+    <textarea
+      placeholder-style="color:#F76260"
+      placeholder="请填写您的个人优势..."
+      auto-focus
+      maxlength="300"
+      v-model="advantage"
+      style="border: 1rpx solid gray; width: 100%; min-height: 300px;"
+    ></textarea>
+    <view class="f-horizon-center">
+      <button type="primary" size="default" class="send-button"  @click="submit">提 交</button>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { saveResumeAdvantage } from '@/api/user'
+import { ref } from 'vue'
+import { userStore } from '@/store/user'; const useUserStore = userStore()
+
+const advantage = ref('')
+// 获取基础信息-
+function getBaseInfo () {
+  const baseInfo = useUserStore.baseInfo
+  advantage.value = baseInfo?.advantage || ''
+}
+
+// 获取基础信息
+getBaseInfo()
+// 提交
+const submit = async () => {
+  if (!advantage.value) {
+    uni.showToast({ title: '请填写您的个人优势', icon: 'none' })
+    return
+  }
+  await saveResumeAdvantage({ content: advantage.value })
+  uni.showToast({ title: '编辑成功', icon: 'success' })
+  await useUserStore.getInfo()
+  //
+  setTimeout(() => {
+		uni.navigateBack({
+			delta: 1,
+		})
+	}, 1000);
+}
+</script>
+<style lang="scss" scoped>
+</style>

+ 214 - 0
pagesA/resumeOnline/baseInfoEdit.vue

@@ -0,0 +1,214 @@
+<template>
+	<view class="f-straight wrapper">
+		<uni-forms ref="form" :modelValue="formData" :rules="rules" validateTrigger="bind" label-width="105px" label-align="right">
+			<uni-forms-item label="头像" name="avatar" class="f-straight" required>
+        <view style="display: flex;flex-wrap: wrap;">
+          <view class="upload-img" v-if="formData?.avatar">
+            <uni-icons size="35" type="clear" color="#fe574a" style="position: absolute;right: -15px; top: -15px; z-index: 9" @click="formData.avatar = ''"></uni-icons>
+            <image :src="formData?.avatar" mode="contain" style="width: 200rpx;height: 200rpx;" @click="handlePreviewImage"></image>
+          </view>
+          <view v-else class="upload-file" @click="uploadPhotos">
+            <uni-icons type="plusempty" size="50" color="#f1f1f1"></uni-icons>
+          </view>
+        </view>
+			</uni-forms-item>
+			<uni-forms-item required label="中文名" name="name">
+        <uni-easyinput v-model="formData.name" placeholder="请输入中文名" />
+			</uni-forms-item>
+      <uni-forms-item label="性别" name="sex" required>
+				<uni-data-checkbox v-model="formData.sex" :localdata="sexData" />
+			</uni-forms-item>
+      <uni-forms-item label="联系电话" name="phone" clearable>
+        <uni-easyinput v-model="formData.phone" placeholder="请输入电话号码" />
+			</uni-forms-item>
+      <uni-forms-item label="常用邮箱" name="email" clearable required>
+        <uni-easyinput v-model="formData.email" placeholder="请输入常用邮箱" />
+			</uni-forms-item>
+      <uni-forms-item required label="出生日期" name="birthday">
+        <uni-datetime-picker type="date" return-type="timestamp" v-model="formData.birthday" />
+			</uni-forms-item>
+      <uni-forms-item label="首次工作时间" name="firstWorkTime">
+        <uni-datetime-picker type="date" return-type="timestamp" v-model="formData.firstWorkTime" />
+			</uni-forms-item>
+			<uni-forms-item required label="工作年限" name="expType" >
+				<uni-data-picker v-model="formData.expType" :localdata="dictObj.exp" :clear-icon="false" popup-title="请选择工作年限" :clear="false" :map="map"></uni-data-picker>
+			</uni-forms-item>
+      <uni-forms-item required label="最高学历" name="eduType" >
+				<uni-data-picker v-model="formData.eduType" :localdata="dictObj.edu" :clear-icon="false" popup-title="请选择最高学历" :clear="false" :map="map"></uni-data-picker>
+			</uni-forms-item>
+			<uni-forms-item required label="求职类型" name="jobType" >
+				<uni-data-picker v-model="formData.jobType" :localdata="dictObj.jobType" :clear-icon="false" popup-title="请选择求职类型" :map="map"></uni-data-picker>
+			</uni-forms-item>
+      <uni-forms-item required label="求职状态" name="jobStatus" >
+				<uni-data-picker v-model="formData.jobStatus" :localdata="dictObj.jobStatus" :clear-icon="false" popup-title="请选择求职状态" :map="map"></uni-data-picker>
+			</uni-forms-item>
+      <uni-forms-item label="婚姻状况" name="maritalStatus" >
+				<uni-data-picker v-model="formData.maritalStatus" :localdata="dictObj.marital" :clear-icon="true" popup-title="请选择婚姻状况" :map="map"></uni-data-picker>
+			</uni-forms-item>
+      <uni-forms-item label="所在城市" name="areaId" >
+				<uni-data-picker v-model="formData.areaId" :localdata="dictObj.areaTreeData" :clear-icon="true" popup-title="请选择所在城市" :map="{ text: 'name', value: 'id'}"></uni-data-picker>
+			</uni-forms-item>
+      <uni-forms-item label="户籍地" name="regId" >
+				<uni-data-picker v-model="formData.regId" :localdata="dictObj.areaTreeData" :clear-icon="true" popup-title="请选择户籍所在地" :map="{ text: 'name', value: 'id'}"></uni-data-picker>
+			</uni-forms-item>
+			<view class="f-horizon-center">
+				<button type="primary" size="default" class="send-button"  @click="submit">提 交</button>
+			</view>
+		</uni-forms>
+	</view>
+</template>
+
+<script setup>
+import { ref, unref } from 'vue'
+import { userStore } from '@/store/user'
+import { dictObj } from '@/utils/position.js'
+import { uploadFile } from '@/api/file'
+import { cloneDeep } from 'lodash-es'
+import { saveBaseInfo, updatePersonAvatar } from '@/api/user'
+import { emailRequired } from '@/utils/validate'
+
+const form = ref()
+const sexData = ref([])
+const map = { text: 'label', value: 'value' }
+const useUserStore = userStore()
+
+const formData = ref({})
+const getInfo = () => {
+  formData.value = cloneDeep(useUserStore?.baseInfo) || {
+    avatar: '',
+    name: '',
+    sex: '',
+    regId: '',
+    birthday: '1985-05-01',
+    phone: '',
+    email: '',
+    eduType: '',
+    firstWorkTime: '',
+    expType: '',
+    jobType: '',
+    jobStatus: '',
+    maritalStatus: '',
+    areaId: ''
+  }
+  // 根据字典数据初始化地区数据
+  if (dictObj && dictObj?.areaTreeData) {
+    const type = typeof dictObj.areaTreeData[0].id
+    formData.value.regId = type === 'string' ? formData.value.regId.toString() : Number(formData.value.regId)
+    formData.value.areaId = type === 'string' ? formData.value.areaId.toString() : Number(formData.value.areaId)
+  }
+}
+getInfo()
+
+if (dictObj && dictObj?.sex) {
+  sexData.value = dictObj.sex.map(e => {
+    return { text: e.label, value: e.value, ...e }
+  })
+}
+
+// 图片预览
+const handlePreviewImage = () => {
+  uni.previewImage({
+    current: 0,
+    urls: [formData.value.avatar]
+  })
+}
+
+// 选择头像
+const uploadPhotos = () => {
+  wx.chooseImage({
+    count: 1,
+    sizeType: ['original', 'compressed'],
+    sourceType: ['album', 'camera'],
+    success: function(res){
+      const size = res.tempFiles[0]?.size || 0
+      if (size >= 31457280) {
+        uni.showToast({
+          icon: 'none',
+          title: '头像上传大小不得超过 20MB !',
+          duration: 2000
+        })
+        return
+      }
+      const path = res.tempFilePaths[0]
+      uploadFile(path, 'img').then(res => {
+        formData.value.avatar = res.data
+      }).catch(error => {
+        uni.showToast({
+          icon: 'error',
+          title: '图片上传失败!',
+          duration: 2000
+        })
+      })
+    }
+  })
+}
+
+const rules = {
+	avatar:{
+		rules: [{required: true, errorMessage: '请上传头像' }]
+	},
+	name:{
+		rules: [{required: true, errorMessage: '请输入姓名' }]
+	},
+	sex : {
+		rules: [{required: true, errorMessage: '请选择您的性别' }]
+	},
+	birthday: {
+		rules: [{required: true, errorMessage: '请选择您的出生日期' }]
+	},
+  email: emailRequired,
+  expType: {
+		rules: [{required: true, errorMessage: '请选择您的工作年限' }]
+	},
+  eduType: {
+		rules: [{required: true, errorMessage: '请选择您的最高学历' }]
+	},
+  jobType: {
+		rules: [{required: true, errorMessage: '请选择您的求职类型' }]
+	},
+  jobStatus: {
+		rules: [{required: true, errorMessage: '请选择您的求职状态' }]
+	}
+}
+
+const submit = async () => {
+  const valid = await unref(form).validate()
+  if (!valid) return
+
+  await updatePersonAvatar(formData.value.avatar)
+  await saveBaseInfo(formData.value)
+  uni.showToast({ title: '编辑成功', icon: 'success' })
+  await useUserStore.getInfo()
+  getInfo()
+  setTimeout(() => {
+		uni.navigateBack({
+			delta: 1
+		})
+	}, 1000)
+}
+</script>
+
+<style lang="less" scoped>
+
+.wrapper{
+	padding: 15px;
+  padding-top: 30px;
+}
+.upload-img{
+  position: relative;
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+}
+.upload-file{
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 10rpx;
+}
+</style>

+ 94 - 0
pagesA/resumeOnline/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
+}

+ 177 - 0
pagesA/resumeOnline/educationExp.vue

@@ -0,0 +1,177 @@
+<!-- 教育经历 -->
+<template>
+  <view class="ss-m-x-30 ss-m-y-30">
+    <uni-forms ref="form" :modelValue="formData" :rules="rules" validateTrigger="bind" label-width="90px">
+      <uni-forms-item label="学校名称" name="schoolName" required>
+				<uni-combox :candidates="schoolData" placeholder="学校名称" v-model="formData.schoolName" @input="handleSearchSchool"></uni-combox>
+			</uni-forms-item>
+      <uni-forms-item label="所学专业" name="major" required>
+				<uni-combox :candidates="majorData" placeholder="所学专业" v-model="formData.major" @input="handleSearchMajor"></uni-combox>
+			</uni-forms-item>
+      <uni-forms-item label="学历" name="educationType" required>
+        <uni-data-select v-model="formData.educationType" :localdata="searchData.eduType"></uni-data-select>
+			</uni-forms-item>
+      <uni-forms-item label="学制类型" name="educationSystemType" required>
+        <uni-data-select v-model="formData.educationSystemType" :localdata="searchData.eduSystemType"></uni-data-select>
+			</uni-forms-item>
+      <uni-forms-item label="开始时间" name="startTime" required>
+				<picker mode="date" :value="formData.startTime" fields="month" :end="endDate" @change="e => formData.startTime = e.detail.value">
+					<view class="uni-input ss-m-t-20">{{ formData.startTime }}</view>
+				</picker>
+			</uni-forms-item>
+      <uni-forms-item label="结束时间" name="endTime" required>
+				<picker mode="date" :value="formData.endTime" fields="month" @change="e => formData.endTime = e.detail.value">
+					<view class="uni-input ss-m-t-20">{{ formData.endTime }}</view>
+				</picker>
+			</uni-forms-item>
+      <uni-forms-item label="在校经历" name="content">
+				<uni-easyinput type="textarea" v-model="formData.content" autoHeight  placeholder="请输入内容"></uni-easyinput>
+			</uni-forms-item>
+    </uni-forms>
+    <view class="f-horizon-center">
+      <button v-if="editId" size="default" class="delete-button commonBtnStyle" @click="handleDelete">删 除</button>
+      <button size="default" :class="{'save-button': editId, 'commonBtnStyle': editId, 'send-button': !editId}"  @click="submit">保 存</button>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, unref } from 'vue'
+import { dictObj } from '@/utils/position.js'
+import { convertYearMonthToTimestamp, timesTampChange } from '@/utils/date.js'
+import { schoolSearchByName, schoolMajorByName, saveResumeEduExp, getResumeEduExp, deleteResumeEduExp } from '@/api/resume.js'
+import { onLoad } from '@dcloudio/uni-app'
+import { cloneDeep } from 'lodash-es'
+
+let formData = ref({
+  startTime: '2014-01',
+  endTime: '2018-01'
+})
+const editId = ref(null)
+const majorData = ref([])
+const schoolData = ref([])
+const form = ref()
+const date = new Date()
+const endDate = date.getFullYear() + '-' + (date.getMonth() + 1) // 不可选时间
+const searchData = ref({
+  school: [],
+  major: [],
+  eduType: dictObj.edu.map(e => ({ text: e.label, value: e.value})),
+  eduSystemType: dictObj.eduSystemType.map(e => ({ text: e.label, value: e.value}))
+})
+
+const rules = {
+	schoolName:{
+		rules: [{required: true, errorMessage: '请输入学校名称' }]
+	},
+	major:{
+		rules: [{required: true, errorMessage: '请输入所学专业' }]
+	},
+	educationType:{
+		rules: [{required: true, errorMessage: '请选择学历' }]
+	},
+	educationSystemType:{
+		rules: [{required: true, errorMessage: '请选择学制类型' }]
+	},
+	startTime:{
+		rules: [{required: true, errorMessage: '请选择开始时间' }]
+	},
+	endTime:{
+		rules: [{required: true, errorMessage: '请选择结束时间' }]
+	}
+}
+const getEduExp = async (id) => {
+  const { data } = await getResumeEduExp()
+  if (!data || !data.length) {
+    return
+  }
+  const obj = data.find(e => e.id == id)
+  formData.value = cloneDeep(obj)
+  formData.value.startTime = obj.startTime ? timesTampChange(obj.startTime, 'Y-M') : '2014-01'
+  formData.value.endTime = obj.endTime ? timesTampChange(obj.endTime, 'Y-M') : '2018-01'
+  handleSearchSchool(obj.schoolName)
+  handleSearchMajor(obj.major)
+}
+
+onLoad((options) => {
+  if (options.id) {
+    editId.value = options.id
+    getEduExp(options.id)
+  }
+})
+
+// 学校搜索
+const handleSearchSchool = (e) => {
+  if (!e) return schoolData.value = []
+  schoolSearchByName({ name: e }).then(res => {
+    searchData.value.school = res.data
+    schoolData.value = res.data && res.data?.length ? res.data.map(e => e.value) : []
+  })
+}
+
+// 专业搜索
+const handleSearchMajor = (e) => {
+  if (!e) return majorData.value = []
+  schoolMajorByName({ name: e }).then(res => {
+    searchData.value.major = res.data
+    majorData.value = res.data && res.data?.length ? res.data.map(e => e.nameCn) : []
+  })
+}
+
+// 保存
+const submit = async () => {
+  const valid = await unref(form).validate()
+  if (!valid) return
+  formData.value.majorId = searchData.value.major.find(e => e.nameCn === formData.value.major)?.id
+  formData.value.schoolId = searchData.value.school.find(e => e.value === formData.value.schoolName)?.key
+  try {
+    const startTime = convertYearMonthToTimestamp(formData.value.startTime)
+    const endTime = convertYearMonthToTimestamp(formData.value.endTime)
+    if (startTime > endTime) {
+      uni.showToast({ icon: 'none', title: '开始时间不能大于结束时间' })
+      return
+    }
+    await saveResumeEduExp({ ...formData.value, startTime, endTime })
+    uni.showToast({
+			icon: 'success',
+			title: '保存成功'
+		})
+		setTimeout(() => {
+      editId.value = null
+      uni.navigateBack({
+        delta: 1
+      })
+    }, 1000)
+  } catch (err) {
+		uni.showToast({
+			icon: 'none',
+			title: err.msg
+		})
+	}
+}
+
+// 删除
+const handleDelete = async () => {
+	try {
+		await deleteResumeEduExp(editId.value)
+		uni.showToast({
+			icon: 'success',
+			title: '删除成功'
+		})
+		setTimeout(() => {
+			editId.value = null
+			uni.navigateBack({
+				delta: 1
+			})
+		}, 1000)
+	} catch (err) {
+		uni.showToast({
+			icon: 'none',
+			title: err.msg
+		})
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 637 - 0
pagesA/resumeOnline/index.vue

@@ -0,0 +1,637 @@
+<template>
+  <layout-page>
+    <!-- 基本信息 -->
+    <view class="progress-box">
+      <view>简历完成度</view>
+      <progress
+        :percent="Number.isInteger(completeNum/7*100) ? (completeNum/7*100) : (completeNum/7*100).toFixed(2)"
+        show-info
+        border-radius="50"
+        activeColor="#00B760"
+        stroke-width="8"
+      />
+    </view>
+    <!-- <view class="topTip">
+      使用简历附件生成在线简历:
+      <text class="resumeAnalysisBtn" @tap="handleToResumeAnalysis">立即生成</text>
+    </view> -->
+    <view class="baseInfo borderLine" @tap="handleTo('baseInfoEdit')">
+      <view>
+        <view class="baseInfo-name">
+          <text class="name title">{{ baseInfo.name }}</text>
+          <uni-icons
+            type="icon-Edit"
+            color="#333"
+            custom-prefix="iconfont"
+            size="20"
+          ></uni-icons>
+        </view>
+        <view class="baseInfo-desc">{{ baseInfo.jobStatusText }}</view>
+        <view class="baseInfo-desc">{{ baseInfo.expTypeText ? baseInfo.expTypeText + ' - ' : '' }}{{ baseInfo.age ? baseInfo.age + '岁' : '' }}{{ baseInfo.eduTypeText ? ' - ' + baseInfo.eduTypeText : '' }}</view>
+        <view class="baseInfo-phone">
+          <uni-icons
+            type="icon-Phone"
+            color="#999"
+            custom-prefix="iconfont"
+            size="14"
+          ></uni-icons>
+          <text class="number">{{ baseInfo.phone }}</text>
+        </view>
+      </view>
+      <view class="head">
+        <image
+          :src="getUserAvatar(baseInfo?.avatar, baseInfo?.sex)"
+          mode="scaleToFill"
+        />
+      </view>
+    </view>
+    <!-- 个人画像 -->
+    <view class="characteristic borderLine">
+      <view class="titleBox">
+        <text class="title">个人画像</text>
+        <uni-icons
+          type="icon-Edit"
+          color="#666"
+          custom-prefix="iconfont"
+          size="18"
+          @tap="handleTo('portrait')"
+        ></uni-icons>
+      </view>
+      <view class="tags">
+        <view
+          v-for="tag in baseInfo.tagList"
+          :key="tag"
+          class="tag"
+        >
+          {{ tag }}
+        </view>
+      </view>
+    </view>
+    <!-- 个人优势 -->
+    <view class="advantage borderLine">
+      <view class="titleBox">
+        <text class="title">个人优势</text>
+        <uni-icons
+          type="icon-Edit"
+          color="#666"
+          custom-prefix="iconfont"
+          size="18"
+          @tap="handleTo('advantage')"
+        ></uni-icons>
+      </view>
+      <view class="ellipsis-2 text px-20">{{ baseInfo.advantage ? baseInfo.advantage : '请填写您的个人优势...' }}</view>
+    </view>
+    <!-- 求职意向 -->
+    <view class="intention borderLine">
+      <view class="titleBox">
+        <text class="title">求职意向</text>
+        <uni-icons
+          type="icon-add"
+          color="#666"
+          custom-prefix="iconfont"
+          size="18"
+          @tap="handleTo('jobIntention')"
+        ></uni-icons>
+      </view>
+      <view class="content">
+        <view class="text" v-if="!intention.length">请填写您的求职意向...</view>
+        <uni-list :border="false">
+          <uni-list-item
+            v-for="int in intention"
+            :key="int.id"
+            :border="false"
+            :clickable="true"
+            showArrow
+            @click="handleTo('jobIntention', int.id)"
+          >
+            <template v-slot:body>
+              <view class="item">
+                <view class="item-title">{{ int.jobTypeName}}</view>
+                <view>
+                  <text class="mr-20">{{ int.position}}</text>
+                  <text>{{ int.payFrom }} {{ int.payFrom  && int.payTo ? '-' : ''}} {{ int.payTo}}</text>
+                </view>
+                <view>{{ int.interestedArea && int.interestedArea.length ? int.workArea + ',' + int.interestedArea.map(e => e.name).join(',') : int.workArea }}</view>
+                <view class="item-tags">
+                  <view v-for="industry in int.industry" :key="industry.id" class="tag">{{ industry.nameCn }}</view>
+                </view>
+              </view>
+            </template>
+          </uni-list-item>
+        </uni-list>
+      </view>
+    </view>
+    <!-- 教育经历 -->
+    <view class="educationExp borderLine">
+      <view class="titleBox">
+        <text class="title">教育经历</text>
+        <uni-icons
+          type="icon-add"
+          color="#666"
+          custom-prefix="iconfont"
+          size="18"
+          @tap="handleTo('educationExp')"
+        ></uni-icons>
+      </view>
+      <view class="content">
+        <view class="text" v-if="!educationExp.length">请填写您的教育经历...</view>
+        <uni-list :border="false">
+          <uni-list-item
+            v-for="education in educationExp"
+            :key="education.id"
+            showArrow
+            :border="false"
+            :clickable="true"
+            :title="education.schoolName"
+            :note="`${education.major} ${education.educationTypeText}`"
+            :rightText="education.time"
+            @click="handleTo('educationExp', education.id)"
+          />
+        </uni-list>
+      </view>
+    </view>
+    <!-- 工作经历 -->
+    <view class="workExp borderLine">
+      <view class="titleBox">
+        <text class="title">工作经历</text>
+        <uni-icons
+          type="icon-add"
+          color="#666"
+          custom-prefix="iconfont"
+          size="18"
+          @tap="handleTo('workExperience')"
+        ></uni-icons>
+      </view>
+      <view class="content">
+        <view
+          v-for="work in workExp"
+          :key="work.id"
+          class="content-item"
+          @tap="handleTo('workExperience', work.id)"
+        >
+          <view class="content-title">
+            <view class="name">{{ work.enterpriseName }}</view>
+            <view class="time">
+              {{ work.time }}
+              <uni-icons
+                class="icon"
+                type="right"
+                color="#aaa"
+                size="16"
+              />
+            </view>
+          </view>
+          <view class="content-subTitle">{{ work.positionName }}</view>
+          <view class="content-main ellipsis-2">内容:{{ work.content }}</view>
+        </view>
+      </view>
+    </view>
+    <!-- 培训经历 -->
+    <view class="workExp trainExp borderLine">
+      <view class="titleBox">
+        <text class="title">培训经历</text>
+        <uni-icons
+          type="icon-add"
+          color="#666"
+          custom-prefix="iconfont"
+          size="18"
+          @tap.stop="handleTo('trainingExperience')"
+        ></uni-icons>
+      </view>
+      <view class="content">
+        <view
+          v-for="train in trainExp"
+          :key="train.id"
+          class="content-item"
+          @tap.stop="handleTo('trainingExperience', train.id)"
+        >
+          <view class="content-title">
+            <view class="name">{{ train.orgName }}</view>
+            <view class="time">
+              {{ train.time }}
+              <uni-icons
+                class="icon"
+                type="right"
+                color="#aaa"
+                size="16"
+              />
+            </view>
+          </view>
+          <view class="content-subTitle">课程:{{ train.course }}</view>
+          <view class="content-main ellipsis-2">描述:{{ train.content }}</view>
+        </view>
+      </view>
+    </view>
+    <!-- 职业技能 -->
+    <view class="characteristic">
+      <view class="titleBox">
+        <text class="title">职业技能</text>
+        <uni-icons
+          type="icon-Edit"
+          color="#666"
+          custom-prefix="iconfont"
+          size="18"
+          @tap.stop="handleTo('vocationalSkills')"
+        ></uni-icons>
+      </view>
+      <view class="tags">
+        <view
+          v-for="skill in skillExp"
+          :key="skill.title"
+          class="tag"
+        >
+          {{ skill.title }}
+        </view>
+      </view>
+    </view>
+  </layout-page>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { getAgeByBirthdayTimestamp, timesTampChange } from '@/utils/date'
+import {
+  getResumeJobInterested,
+  getResumeEduExp,
+  getResumeWorkExp,
+  getResumeTrainExp,
+  getResumePersonSkill
+} from '@/api/resume'
+import { getText } from '@/utils/getText'
+import { getDict } from '@/hooks/useDictionaries'
+import { userStore } from '@/store/user'
+import { dealJobData } from './dict'
+import layoutPage from '@/layout'
+import { onShow } from '@dcloudio/uni-app'
+import { getUserAvatar } from '@/utils/avatar'
+
+const useUserStore = userStore()
+
+const baseInfo = ref({})
+const intention = ref([])
+const educationExp = ref([])
+const workExp = ref([])
+const trainExp = ref([])
+const skillExp = ref([])
+// 简历完成度
+const completeNum = ref(0)
+
+function handleTo (str, id) {
+  uni.navigateTo({ url: id ? `/pagesA/resumeOnline/${str}?id=${id}` : `/pagesA/resumeOnline/${str}` })
+}
+
+function handleToResumeAnalysis () {
+  uni.navigateTo({ url: '/pagesA/resumeAnalysis/index' })
+}
+
+// 获取基础信息
+function getBaseInfo () {
+  const { name, phone, ...obj } = useUserStore.baseInfo
+  baseInfo.value = {
+    ...obj,
+    name: name ? name : useUserStore.userInfo.phone,
+    phone: phone ? phone : useUserStore.userInfo.phone,
+    age: obj.birthday ? getAgeByBirthdayTimestamp(obj.birthday) : 0
+  }
+  if (useUserStore.userInfo && Object.keys(useUserStore.userInfo).length) completeNum.value++
+  if (baseInfo.value?.advantage ) completeNum.value++
+}
+
+// 获取求职意向
+async function getJobInterested () {
+  intention.value = []
+  const { data } = await getResumeJobInterested()
+  if (!data || !data.length) {
+    return
+  }
+  completeNum.value++ // 完成度展示
+  intention.value = dealJobData(data)
+}
+
+// 获取教育经历
+async function getEduExp () {
+  educationExp.value = []
+  const { data: dict } = await getDict('menduner_education_type')
+  if (dict.code !== 0) {
+    return
+  }
+  const { data } = await getResumeEduExp()
+  if (!data || !data.length) {
+    return
+  }
+  completeNum.value++ // 完成度展示
+  educationExp.value = data.map(e => {
+    const item = dict.data.find(_e => _e.value === e.educationType)
+    return {
+      ...e,
+      educationTypeText: item?.label ?? '',
+      time: `${timesTampChange(e.startTime ,'Y-M')}-${timesTampChange(e.endTime ,'Y-M')} `
+    }
+  })
+}
+
+// 获取工作经验
+async function getWorkExp () {
+  workExp.value = []
+  const { data } = await getResumeWorkExp()
+  if (!data || !data.length) {
+    return
+  }
+  completeNum.value++ // 完成度展示
+  workExp.value = data.map(e => {
+    return {
+      ...e,
+      time: `${timesTampChange(e.startTime ,'Y-M')}-${e.endTime ? timesTampChange(e.endTime ,'Y-M') : '至今'} `
+    }
+  })
+}
+
+// 培训经历
+async function getTrainExpData () {
+  trainExp.value = []
+  const { data } = await getResumeTrainExp()
+  if (!data || !data.length) {
+    return
+  }
+  completeNum.value++ // 完成度展示
+  trainExp.value = data.map(e => {
+    return {
+      ...e,
+      time: `${timesTampChange(e.startTime ,'Y-M')}-${e.endTime ? timesTampChange(e.endTime ,'Y-M') : '至今'} `
+    }
+  })
+}
+
+// 职业技能
+async function getSkillExpData () {
+  skillExp.value = []
+  const { data: _skillList} = await getDict('skillList', {}, 'skillList')
+  const skillList = _skillList?.data
+  if (!skillList || !skillList.length) {
+    return
+  }
+
+  completeNum.value++ // 完成度展示
+  const { data: _skillLevelArr } = await getDict('menduner_skill_level')
+  const skillLevelArr = _skillLevelArr?.data
+  if (!skillLevelArr || !skillLevelArr.length) {
+    return
+  }
+
+  const { data } = await getResumePersonSkill()
+  if (!data || !data.length) {
+    return
+  }
+  skillExp.value = data.map(e => {
+    return {
+      ...e,
+      title: `${getText(e.skillId, skillList, 'nameCn', 'id')} / ${getText(e.level, skillLevelArr)}`
+    }
+  })
+}
+
+onShow(() => {
+  completeNum.value = 0
+  // 获取基础信息
+  getBaseInfo()
+  // 获取求职意向
+  getJobInterested()
+  // 获取教育经历
+  getEduExp()
+  // 获取工作经验
+  getWorkExp()
+  // 培训经历
+  getTrainExpData()
+  // 职业技能
+  getSkillExpData()
+})
+</script>
+
+<style lang="scss" scoped>
+$px: 30rpx;
+.borderLine {
+  border-bottom: 2rpx solid #f5f5f5;
+}
+.progress-box {
+  padding: 20rpx $px;
+}
+.title {
+  font-size: 40rpx;
+  font-weight: 600;
+}
+.flex-1 {
+  flex: 1;
+}
+.soloHeight {
+  height: 80rpx;
+  line-height: 80rpx;
+}
+.px-20 {
+  padding-left: 20rpx;
+  padding-right: 20rpx;
+  box-sizing: border-box;
+}
+.mr-20 {
+  margin-right: 20rpx;
+}
+.ellipsis-2 {
+  overflow: hidden;
+  display: -webkit-box;
+  text-overflow: ellipsis; //属性规定当文本溢出包含元素时发生的事情  text-overflow: clip|ellipsis|string; (修剪/省略号/指定字符串)
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical; //属性规定框的子元素应该被水平或垂直排列
+}
+.titleBox {
+  margin-bottom: 10rpx;
+  display: flex;
+  justify-content: space-between;
+}
+.text {
+  font-size: 28rpx;
+  color: #666;
+}
+.baseInfo {
+  padding: 20rpx $px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  &-name {
+    margin-bottom: 10rpx;
+    .name {
+      margin-right: 10rpx;
+    }
+  }
+  &-desc {
+    font-size: 28rpx;
+    color: #666;
+    margin-bottom: 10rpx;
+  }
+  &-phone {
+    font-size: 28rpx;
+    color: #666;
+    .number {
+      margin-left: 10rpx;
+    }
+  }
+  .head {
+    image {
+      width: 150rpx;
+      height: 150rpx;
+      border: 2rpx solid #ccc;
+      border-radius: 50%;
+    }
+  }
+}
+.advantage {
+  padding: $px;
+}
+
+.characteristic {
+  padding: $px;
+  .tags {
+    padding-top: $px;
+    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;
+    }
+  }
+}
+.intention,.educationExp,.workExp,.projectExp {
+  padding: $px;
+}
+.workExp {
+  .content {
+    &-item {
+      padding: $px 20rpx;
+    }
+    &-title {
+      display: flex;
+      justify-content: space-between;
+      .name {
+        // font-weight: 600;
+        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;
+    }
+  }
+}
+.intention {
+  .content {
+    .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;
+        }
+      }
+    }
+  }
+}
+
+
+.popup {
+  padding: $px;
+  padding-bottom: 100rpx;
+  &-title {
+    width: 100%;
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: $px;
+    font-size: 24rpx;
+    .title {
+      font-size: 36rpx;
+    }
+  }
+  &-content {
+    .box {
+      display: flex;
+      justify-content: space-between;
+      border-bottom: 2rpx solid #eee;
+      padding: $px 0;
+      font-size: 30rpx;
+      font-weight: 500;
+      color: #000;
+      &:last-of-type {
+        border-bottom: none;
+      }
+      &.active {
+        color: #00B760;
+        font-weight: 600;
+      }
+    }
+  }
+}
+
+.popupBox {
+  height: 90vh;
+  .handleBtnBox {
+    // padding: 0 $px;
+    display: flex;
+    justify-content: space-between;
+    view {
+      padding: calc($px / 2);
+      margin-right: calc($px / 2);
+    }
+    .save {
+      color: #00B760;
+    }
+    .close {
+      color: gray;
+    }
+  }
+  .popupContent {
+    padding: $px;
+    padding-bottom: 100rpx;
+  }
+}
+.topTip {
+  background-color: #f7f8fa;
+  color: #2f3640;
+  padding: 12px 20px;
+  margin: 20px 20rpx;
+  font-size: 14px;
+}
+.resumeAnalysisBtn {
+  text-decoration: underline;
+  color: #00B760;
+}
+</style>

+ 177 - 0
pagesA/resumeOnline/jobIntention.vue

@@ -0,0 +1,177 @@
+<!--  -->
+<template>
+  <view class="ss-m-x-30 ss-m-y-30">
+    <uni-forms ref="form" :modelValue="formData" :rules="rules" validateTrigger="bind">
+      <uni-forms-item label="期望岗位" name="positionId" required label-width="90px">
+				<uni-data-picker popup-title="请选择期望岗位" v-model="formData.positionId" :localdata="dictObj?.positionTreeData || []" :clear-icon="false" :map="{ text: 'nameCn', value: 'id'}"></uni-data-picker>
+			</uni-forms-item>
+      <uni-forms-item label="期望行业" name="industryIdList" required label-width="90px">
+				<m-select
+					label="请选择期望行业"
+					:items="dictObj?.industryTreeData || []"
+					item-label="nameCn"
+					item-value="id"
+					multiple
+					v-model="formData.industryIdList"
+				></m-select>
+				<!-- <uni-data-picker popup-title="请选择期望行业" v-model="formData.industryIdList" :localdata="dictObj?.industryTreeData || []" :clear-icon="false" :map="{ text: 'nameCn', value: 'id'}"></uni-data-picker> -->
+			</uni-forms-item>
+      <uni-forms-item label="最低薪资" name="payFrom" required label-width="90px">
+        <uni-number-box v-model="formData.payFrom" :min="1" :max="999999999" :step="1000" :width="150" @change="payChange"></uni-number-box>
+			</uni-forms-item>
+      <uni-forms-item label="最高薪资" name="payTo" required label-width="90px">
+        <uni-number-box v-model="formData.payTo" :min="payToMin" :max="999999999" :step="1000" :width="150"></uni-number-box>
+			</uni-forms-item>
+      <uni-forms-item label="求职类型" name="jobType" required label-width="90px">
+				<uni-data-picker popup-title="请选择求职类型" v-model="formData.jobType" :localdata="dictObj?.jobType || []" :clear-icon="false" :map="{ text: 'label', value: 'value'}"></uni-data-picker>
+			</uni-forms-item>
+      <uni-forms-item label="工作城市" name="workAreaId" required label-width="90px">
+				<uni-data-picker popup-title="请选择工作城市" v-model="formData.workAreaId" :localdata="dictObj?.areaTreeData || []" :clear-icon="false" :map="{ text: 'name', value: 'id'}"></uni-data-picker>
+			</uni-forms-item>
+      <uni-forms-item label="其它感兴趣的城市" name="interestedAreaIdList" label-width="90px">
+				<!-- <uni-data-picker popup-title="其它感兴趣的城市" v-model="formData.interestedAreaIdList" :localdata="dictObj?.areaTreeData || []" :clear-icon="false" :map="{ text: 'name', value: 'id'}"></uni-data-picker> -->
+				<m-select
+					label="请选择其它感兴趣的城市"
+					:items="dictObj?.areaTreeData || []"
+					item-label="name"
+					item-value="id"
+					multiple
+					v-model="formData.interestedAreaIdList"
+				>
+				</m-select>
+			</uni-forms-item>
+    </uni-forms>
+    <view class="f-horizon-center">
+      <button v-if="editId" size="default" class="delete-button commonBtnStyle" @click="handleDelete">删 除</button>
+      <button size="default" :class="{'save-button': editId, 'commonBtnStyle': editId, 'send-button': !editId}"  @click="submit">保 存</button>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, unref } from 'vue'
+import { dictObj } from '@/utils/position.js'
+import { saveResumeJobInterested, getResumeJobInterested, deleteResumeJobInterested } from '@/api/resume.js'
+import { onLoad } from '@dcloudio/uni-app'
+import { cloneDeep } from 'lodash-es'
+import MSelect from '@/components/FilterList/select.vue'
+
+let formData = ref({ positionId: '', payFrom: 0, payTo: 0 })
+const form = ref()
+const editId = ref(null)
+
+// 获取求职意向
+async function getJobInterested (id) {
+  const { data } = await getResumeJobInterested()
+  if (!data || !data.length) {
+    return
+  }
+  const obj = data.find(k => k.id === id)
+	formData.value = cloneDeep(obj)
+	formData.value.interestedAreaIdList = obj.interestedAreaIdList.map(e => +e)
+	if (dictObj && dictObj?.areaTreeData) {
+		const type = typeof dictObj.areaTreeData[0].id
+		formData.value.workAreaId = type === 'string' ? obj.workAreaId.toString() : Number(obj.workAreaId)
+	}
+}
+
+onLoad((options) => {
+	if (options.id) {
+		editId.value = options.id
+		getJobInterested(options.id)
+	}
+})
+
+// 提交
+const submit = async () => {
+  const valid = await unref(form).validate()
+  if (!valid) return
+	// 后续做多选
+	// const query = {...formData.value, industryIdList: formData.value.industryIdList, interestedAreaIdList: formData.value.interestedAreaIdList}
+	try {
+		await saveResumeJobInterested(formData.value)
+		uni.showToast({
+			icon: 'success',
+			title: '保存成功'
+		})
+		setTimeout(() => {
+			editId.value = null
+			uni.navigateBack({
+				delta: 1
+			})
+		}, 1000)
+	} catch (err) {
+		uni.showToast({
+			icon: 'none',
+			title: err.msg
+		})
+	}
+}
+
+// 删除
+const handleDelete = async () => {
+	try {
+		await deleteResumeJobInterested(editId.value)
+		uni.showToast({
+			icon: 'success',
+			title: '删除成功'
+		})
+		setTimeout(() => {
+			editId.value = null
+			uni.navigateBack({
+				delta: 1
+			})
+		}, 1000)
+	} catch (err) {
+		uni.showToast({
+			icon: 'none',
+			title: err.msg
+		})
+	}
+}
+
+const rules = {
+	positionId:{
+		rules: [{required: true, errorMessage: '请选择期望岗位' }]
+	},
+	industryIdList:{
+		rules: [{required: true, errorMessage: '请选择期望行业' }]
+	},
+	payFrom:{
+		rules: [{required: true, errorMessage: '请输入薪资最低要求' }]
+	},
+	payTo:{
+		rules: [{required: true, errorMessage: '请输入薪资最高要求' }]
+	},
+	jobType:{
+		rules: [{required: true, errorMessage: '请选择求职类型' }]
+	},
+	workAreaId:{
+		rules: [{required: true, errorMessage: '请选择工作城市' }]
+	},
+}
+
+const payToMin = ref(1)
+const payChange = (val) => {
+  payToMin.value = val
+  if (val > formData.value.payTo) formData.value.payTo = val
+}
+</script>
+
+<style lang="scss" scoped>
+.content {
+	position: relative;
+	&-cover {
+		position: absolute;
+		width: 100%;
+		height: 100%;
+		left: 0;
+		top: 0;
+		z-index: 3;
+	}
+}
+:deep(.uni-forms-item__content) {
+	max-width: 100%;
+	overflow: hidden;
+}
+</style>

+ 131 - 0
pagesA/resumeOnline/portrait.vue

@@ -0,0 +1,131 @@
+<template>
+  <view class="box">
+    <!-- 已选中 -->
+    <view class="chose borderLine">
+      <view class="choseTitle">已选中标签</view>
+      <view class="tags">
+        <view
+          v-for="tag in select"
+          :key="tag"
+          class="tag"
+          style="color: orange; border: 2rpx solid orange;"
+          @tap="handleCancelSelect(tag)"
+        >
+          {{ tag }}
+          <uni-icons type="clear" size="16" color="#ea8d03"></uni-icons>
+        </view>
+      </view>
+    </view>
+    <!-- 选择项 -->
+    <view v-if="showTagList && tagList?.length" class="list">
+      <uni-collapse v-model="collapseOpen">
+        <uni-collapse-item
+          v-for="val in tagList" :key="val.id"
+          :name="val.id"
+          :title="val?.nameCn || '--'"
+        >
+          <view v-if="val?.children?.length" class="tags">
+            <view v-for="k in val.children" :key="k.id">
+              <view v-if="select.includes(k.nameCn)" class="tag" style="color: grey; border: 2rpx solid grey;">
+                <uni-icons type="plusempty" size="14" color="#00B760"></uni-icons>
+                {{ k?.nameCn || '--' }}
+              </view>
+              <view v-else class="tag" style="color: #00B760; border: 2rpx solid #00B760;" @tap="handleSelect(k.nameCn)">
+                <uni-icons type="plusempty" size="14" color="#00B760"></uni-icons>
+                {{ k?.nameCn || '--' }}
+              </view>
+            </view>
+          </view>
+        </uni-collapse-item>
+      </uni-collapse>
+    </view>
+    <view class="f-horizon-center">
+      <button type="primary" size="default" class="send-button"  @click="submit">提 交</button>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { getTagTreeDataApi, savePersonPortrait } from '@/api/user'
+import { ref } from 'vue'
+import { userStore } from '@/store/user'; const useUserStore = userStore()
+
+
+// 选择
+const handleSelect = (nameCn) => {
+  const result = select.value.includes(nameCn)
+  if (!result) return select.value.push(nameCn)
+  else select.value = select.value.filter(e => e !== nameCn)
+}
+
+// 删除
+const handleCancelSelect = (nameCn) => {
+  select.value = select.value.filter(e => e !== nameCn)
+}
+
+const select = ref([])
+// 获取基础信息
+function getBaseInfo () {
+  const baseInfo = useUserStore.baseInfo
+  select.value = baseInfo.tagList &&  baseInfo.tagList?.length ? baseInfo.tagList : []
+}
+
+// 获取基础信息
+getBaseInfo()
+
+const tagList = ref([])
+const collapseOpen = ref([])
+const showTagList = ref(false)
+// 获取标签字典数据
+const getTagList = async () => {
+  showTagList.value = false
+  const res = await getTagTreeDataApi({ type: 0 })
+  tagList.value = res?.data || []
+  collapseOpen.value = tagList.value?.map(e => e.id)
+  showTagList.value = true
+}
+getTagList()
+
+// 提交
+const submit = async () => {
+  await savePersonPortrait({ tagList: select.value })
+  uni.showToast({ title: '编辑成功', icon: 'success' })
+  await useUserStore.getInfo()
+  //
+  setTimeout(() => {
+		uni.navigateBack({
+			delta: 1,
+		})
+	}, 1000);
+}
+</script>
+
+<style lang="scss" scoped>
+$px: 30rpx;
+.borderLine {
+  border-bottom: 2rpx solid #f5f5f5;
+}
+.box {
+  padding: 20rpx $px;
+  .chose {
+    margin-bottom: $px;
+    .choseTitle {
+      margin-bottom: $px;
+    }
+  }
+  .tags {
+    padding: $px 0;
+    display: flex;
+    flex-wrap: wrap;
+    .tag {
+      margin: 0 15rpx 12rpx 0;
+      border: 2rpx 15rpx #00B760;
+      color: #00B760;
+      white-space: nowrap;
+      padding: 4rpx 10rpx;
+      border-radius: 10rpx;
+      font-size: 24rpx;
+    }
+  }
+}
+</style>

+ 134 - 0
pagesA/resumeOnline/trainingExperience.vue

@@ -0,0 +1,134 @@
+<!-- 培训经历 -->
+<template>
+  <view class="ss-m-x-30 ss-m-y-30">
+    <uni-forms ref="form" :modelValue="formData" :rules="rules" validateTrigger="bind" label-width="90px">
+      <uni-forms-item label="培训中心" name="orgName" required>
+				<uni-easyinput type="text" v-model="formData.orgName" placeholder="请输入内容"></uni-easyinput>
+			</uni-forms-item>
+      <uni-forms-item label="培训课程" name="course" required>
+				<uni-easyinput type="text" v-model="formData.course" placeholder="请输入内容"></uni-easyinput>
+			</uni-forms-item>
+      <uni-forms-item label="开始时间" name="startTime" required>
+				<picker mode="date" :value="formData.startTime" fields="month" :end="endDate" @change="e => formData.startTime = e.detail.value">
+					<view class="uni-input ss-m-t-20">{{ formData.startTime }}</view>
+				</picker>
+			</uni-forms-item>
+      <uni-forms-item label="结束时间" name="endTime" required>
+				<picker mode="date" :value="formData.endTime" fields="month" :end="endDate" @change="e => formData.endTime = e.detail.value">
+          <view class="uni-input ss-m-t-20">{{ formData.endTime }}</view>
+        </picker>
+			</uni-forms-item>
+      <uni-forms-item label="培训描述" name="content">
+				<uni-easyinput type="textarea" v-model="formData.content" autoHeight  placeholder="请输入内容"></uni-easyinput>
+			</uni-forms-item>
+    </uni-forms>
+    <view class="f-horizon-center">
+      <button v-if="editId" size="default" class="delete-button commonBtnStyle" @click="handleDelete">删 除</button>
+      <button size="default" :class="{'save-button': editId, 'commonBtnStyle': editId, 'send-button': !editId}"  @click="submit">保 存</button>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, unref } from 'vue'
+import { convertYearMonthToTimestamp, timesTampChange } from '@/utils/date.js'
+import { saveResumeTrainExp, deleteResumeTrainExp, getResumeTrainExp } from '@/api/resume.js'
+import { onLoad } from '@dcloudio/uni-app'
+import { cloneDeep } from 'lodash-es'
+
+let formData = ref({
+  startTime: '2014-01',
+  endTime: '2018-01'
+})
+const editId = ref(null)
+const form = ref()
+const date = new Date()
+const endDate = date.getFullYear() + '-' + (date.getMonth() + 1) // 不可选时间
+
+const rules = {
+	orgName:{
+		rules: [{required: true, errorMessage: '请输入培训中心' }]
+	},
+	course:{
+		rules: [{required: true, errorMessage: '请输入培训课程' }]
+	},
+	startTime:{
+		rules: [{required: true, errorMessage: '请选择培训开始时间' }]
+	},
+  endTime:{
+		rules: [{required: true, errorMessage: '请选择培训结束时间' }]
+	}
+}
+const getTTrainExp = async (id) => {
+  const { data } = await getResumeTrainExp()
+  if (!data || !data.length) {
+    return
+  }
+  const obj = data.find(e => e.id == id)
+  formData.value = cloneDeep(obj)
+  formData.value.startTime = obj.startTime ? timesTampChange(obj.startTime, 'Y-M') : '2014-01'
+  formData.value.endTime = obj.endTime ? timesTampChange(obj.endTime, 'Y-M') : '2018-01'
+}
+
+onLoad((options) => {
+  if (options.id) {
+    editId.value = options.id
+    getTTrainExp(options.id)
+  }
+})
+
+// 保存
+const submit = async () => {
+  const valid = await unref(form).validate()
+  if (!valid) return
+	const startTime = convertYearMonthToTimestamp(formData.value.startTime)
+	const endTime = convertYearMonthToTimestamp(formData.value.endTime)
+	if (startTime > endTime) {
+    uni.showToast({ icon: 'none', title: '开始时间不能大于结束时间' })
+		return
+	}
+  try {
+		await saveResumeTrainExp({ ...formData.value, startTime, endTime })
+    uni.showToast({
+			icon: 'success',
+			title: '保存成功'
+		})
+		setTimeout(() => {
+      editId.value = null
+      uni.navigateBack({
+        delta: 1
+      })
+    }, 1000)
+  } catch (err) {
+		uni.showToast({
+			icon: 'none',
+			title: err.msg
+		})
+	}
+}
+
+// 删除
+const handleDelete = async () => {
+	try {
+		await deleteResumeTrainExp(editId.value)
+		uni.showToast({
+			icon: 'success',
+			title: '删除成功'
+		})
+		setTimeout(() => {
+			editId.value = null
+			uni.navigateBack({
+				delta: 1
+			})
+		}, 1000)
+	} catch (err) {
+		uni.showToast({
+			icon: 'none',
+			title: err.msg
+		})
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 158 - 0
pagesA/resumeOnline/vocationalSkills.vue

@@ -0,0 +1,158 @@
+<!-- 1职业技能 -->
+<template>
+  <view class="ss-m-x-30 ss-m-y-30">
+    <!-- 已选中 -->
+    <view class="chose borderLine">
+      <view class="choseTitle">当前职业技能</view>
+      <view class="tags">
+        <view
+          v-for="tag in skillExp"
+          :key="tag.id"
+          class="tag"
+          style="color: #00B760; border: 2rpx solid #00B760;"
+          @tap="handleDelete(tag.id)"
+        >
+          {{ tag.title }}
+          <uni-icons type="clear" size="16" color="#00B760"></uni-icons>
+        </view>
+      </view>
+    </view>
+
+    <uni-section title="新增" type="line">
+      <uni-forms ref="form" :modelValue="formData" :rules="rules" validateTrigger="bind" label-width="90px" class="ss-m-t-50">
+        <uni-forms-item label="技能名称" name="skillId" required>
+          <uni-data-picker :localdata="skill" v-model="formData.skillId" :map="{ text: 'nameCn', value: 'id' }" placeholder="技能名称" popup-title="请选择技能名称"></uni-data-picker>
+        </uni-forms-item>
+        <uni-forms-item label="熟练度" name="level" required>
+          <uni-data-picker :localdata="skillLevelArr" v-model="formData.level" :map="{ text: 'label', value: 'value' }" placeholder="熟练度" popup-title="请选择熟练度"></uni-data-picker>
+        </uni-forms-item>
+      </uni-forms>
+      <view class="f-horizon-center">
+        <button size="default" class="send-button" @click="submit">提 交</button>
+      </view>
+    </uni-section>
+  </view>
+</template>
+
+<script setup>
+import { ref, unref } from 'vue'
+import { saveResumePersonSkill, deleteResumePersonSkill, getResumePersonSkill, getSkillTree } from '@/api/resume.js'
+import { getDict } from '@/hooks/useDictionaries'
+import { getText } from '@/utils/getText'
+
+let formData = ref({})
+const form = ref()
+const rules = {
+	skillId:{
+		rules: [{required: true, errorMessage: '请选择技能名称' }]
+	},
+	level:{
+		rules: [{required: true, errorMessage: '请选择熟练度' }]
+	}
+}
+
+// 熟练度
+const skillLevelArr = ref([])
+getDict('menduner_skill_level').then(({ data }) => {
+  data = data.data?.length && data.data || []
+  skillLevelArr.value = data
+})
+const skillList = ref([])
+getDict('skillList', {}, 'skillList').then(({ data }) => {
+  data = data.data?.length && data.data || []
+  skillList.value = data
+})
+
+// 获取 职业技能选项
+const skill = ref([])
+const getSkillTreeFunc = async () => {
+  const { data } = await getSkillTree()
+  skill.value = data || []
+}
+getSkillTreeFunc()
+
+// 职业技能
+const skillExp = ref([])
+async function getSkillExpData () {
+  const { data } = await getResumePersonSkill()
+  if (!data || !data.length) {
+    return
+  }
+  skillExp.value = data.map(e => {
+    return {
+      ...e,
+      title: `${getText(e.skillId, skillList.value, 'nameCn', 'id')} / ${getText(e.level, skillLevelArr.value)}`
+    }
+  })
+}
+getSkillExpData()
+
+// 保存
+const submit = async () => {
+  const valid = await unref(form).validate()
+  if (!valid) return
+  try {
+    await saveResumePersonSkill(formData.value)
+    uni.showToast({
+			icon: 'success',
+			title: '新增成功'
+		})
+		setTimeout(() => {
+      getSkillExpData()
+    })
+  } catch (err) {
+		uni.showToast({
+			icon: 'none',
+			title: err.msg
+		})
+	}
+}
+
+// 删除
+const handleDelete = async (id) => {
+	try {
+		await deleteResumePersonSkill(id)
+		uni.showToast({
+			icon: 'success',
+			title: '删除成功'
+		})
+		setTimeout(() => {
+			getSkillExpData()
+		})
+	} catch (err) {
+		uni.showToast({
+			icon: 'none',
+			title: err.msg
+		})
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.borderLine {
+  border-bottom: 2rpx solid #f5f5f5;
+}
+.chose {
+  margin-bottom: 30rpx;
+  .choseTitle {
+    margin-bottom: 30rpx;
+  }
+}
+.tags {
+  padding: 30rpx 0;
+  display: flex;
+  flex-wrap: wrap;
+  .tag {
+    margin: 0 15rpx 12rpx 0;
+    border: 2rpx 15rpx #00B760;
+    color: #00B760;
+    white-space: nowrap;
+    padding: 4rpx 10rpx;
+    border-radius: 10rpx;
+    font-size: 24rpx;
+  }
+}
+:deep(.uni-section .uni-section-header__decoration) {
+  background-color: #00B760 !important;
+}
+</style>

+ 181 - 0
pagesA/resumeOnline/workExperience.vue

@@ -0,0 +1,181 @@
+<!-- 工作经历 -->
+<template>
+  <view class="ss-m-x-30 ss-m-y-30">
+    <uni-forms ref="form" :modelValue="formData" :rules="rules" validateTrigger="bind" label-width="90px">
+      <uni-forms-item label="企业名称" name="enterpriseName" required>
+				<uni-combox :candidates="enterpriseData" placeholder="企业名称" v-model="formData.enterpriseName" @input="handleSearchEnterprise"></uni-combox>
+			</uni-forms-item>
+      <uni-forms-item label="职位名称" name="positionName" required>
+				<uni-combox :candidates="positionData" placeholder="职位名称" v-model="formData.positionName"></uni-combox>
+			</uni-forms-item>
+      <uni-forms-item label="开始时间" name="startTime" required>
+				<picker mode="date" :value="formData.startTime" fields="month" :end="endDate" @change="e => formData.startTime = e.detail.value">
+					<view class="uni-input ss-m-t-20">{{ formData.startTime }}</view>
+				</picker>
+			</uni-forms-item>
+      <uni-forms-item label="结束时间" name="endTime" required>
+				<view class="d-flex">
+          <picker mode="date" :value="formData.endTime" :disabled="endDisabled" fields="month" :end="endDate" @change="e => formData.endTime = e.detail.value">
+            <view class="uni-input ss-m-t-20" :style="{'opacity': endDisabled ? '0.5' : '1'}">{{ formData.endTime }}</view>
+          </picker>
+          <uni-data-checkbox selectedColor="#00B760" class="ss-m-l-50 ss-m-t-14" multiple v-model="sofar" :localdata="[{ text: '至今', value: 1 }]" @change="handleChangeSofar"></uni-data-checkbox>
+        </view>
+			</uni-forms-item>
+      <uni-forms-item label="工作内容" name="content" required>
+				<uni-easyinput type="textarea" v-model="formData.content" autoHeight  placeholder="请输入内容"></uni-easyinput>
+			</uni-forms-item>
+    </uni-forms>
+    <view class="f-horizon-center">
+      <button v-if="editId" size="default" class="delete-button commonBtnStyle" @click="handleDelete">删 除</button>
+      <button size="default" :class="{'save-button': editId, 'commonBtnStyle': editId, 'send-button': !editId}"  @click="submit">保 存</button>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, unref } from 'vue'
+import { convertYearMonthToTimestamp, timesTampChange } from '@/utils/date.js'
+import { enterpriseSearchByName, saveResumeWorkExp, deleteResumeWorkExp, getResumeWorkExp } from '@/api/resume.js'
+import { onLoad } from '@dcloudio/uni-app'
+import { cloneDeep } from 'lodash-es'
+import { getDict } from '@/hooks/useDictionaries.js'
+
+let formData = ref({
+  startTime: '2014-01',
+  endTime: '2018-01'
+})
+const sofar = ref([])
+const endDisabled = ref(false)
+const editId = ref(null)
+const enterpriseData = ref([])
+const positionData = ref([])
+const form = ref()
+const date = new Date()
+const endDate = date.getFullYear() + '-' + (date.getMonth() + 1) // 不可选时间
+const searchData = ref({
+  enterprise: [],
+  position: []
+})
+
+const rules = {
+	enterpriseName:{
+		rules: [{required: true, errorMessage: '请输入企业名称' }]
+	},
+	positionName:{
+		rules: [{required: true, errorMessage: '请输入职位名称' }]
+	},
+	startTime:{
+		rules: [{required: true, errorMessage: '请选择开始时间' }]
+	},
+  content:{
+		rules: [{required: true, errorMessage: '请输入工作内容' }]
+	}
+}
+const getExp = async (id) => {
+  const { data } = await getResumeWorkExp()
+  if (!data || !data.length) {
+    return
+  }
+  const obj = data.find(e => e.id == id)
+  formData.value = cloneDeep(obj)
+  formData.value.startTime = obj.startTime ? timesTampChange(obj.startTime, 'Y-M') : '2014-01'
+  if (!obj.endTime) {
+    endDisabled.value = true
+    formData.value.endTime = '2018-01'
+    sofar.value = [1]
+  } else formData.value.endTime = timesTampChange(obj.endTime, 'Y-M')
+  handleSearchEnterprise(obj.enterpriseName)
+}
+
+onLoad((options) => {
+  if (options.id) {
+    editId.value = options.id
+    getExp(options.id)
+  }
+})
+
+// 企业搜索
+const handleSearchEnterprise = (e) => {
+  if (!e) return enterpriseData.value = []
+  enterpriseSearchByName({ name: e }).then(res => {
+    searchData.value.enterprise = res.data
+    enterpriseData.value = res.data && res.data?.length ? res.data.map(e => e.value) : []
+  })
+}
+
+let positionTreeChildrenData = []
+getDict('positionTreeData', null, 'positionTreeData').then(({ data }) => {
+  data = data.data?.length && data.data || []
+  data.forEach(e => {
+    if (e?.children?.length) positionTreeChildrenData = positionTreeChildrenData.concat(e.children)
+  })
+  searchData.value.position = positionTreeChildrenData
+  positionData.value = positionTreeChildrenData.map(e => e.nameCn)
+})
+
+// 至今
+const handleChangeSofar = (e) => {
+  const value = e.detail.value.length ? e.detail.value[0] : ''
+  endDisabled.value = value ? true : false
+}
+
+// 保存
+const submit = async () => {
+  const valid = await unref(form).validate()
+  if (!valid) return
+  formData.value.enterpriseId = searchData.value.enterprise.find(e => e.value === formData.value.enterpriseName)?.key
+  formData.value.positionId = searchData.value.position.find(e => e.nameCn === formData.value.positionName)?.id
+
+  const startTime = convertYearMonthToTimestamp(formData.value.startTime)
+  const endTime = sofar.value.length ? null : convertYearMonthToTimestamp(formData.value.endTime)
+  if (!endTime && !sofar.value.length) return uni.showToast({ icon: 'none', title: '请选择结束时间' })
+  if (endTime && startTime > endTime) {
+    uni.showToast({ icon: 'none', title: '开始时间不能大于结束时间' })
+    return
+  }
+
+  try {
+		await saveResumeWorkExp({ ...formData.value, startTime, endTime })
+    uni.showToast({
+			icon: 'success',
+			title: '保存成功'
+		})
+		setTimeout(() => {
+      editId.value = null
+      uni.navigateBack({
+        delta: 1
+      })
+    }, 1000)
+  } catch (err) {
+		uni.showToast({
+			icon: 'none',
+			title: err.msg
+		})
+	}
+}
+
+// 删除
+const handleDelete = async () => {
+	try {
+		await deleteResumeWorkExp(editId.value)
+		uni.showToast({
+			icon: 'success',
+			title: '删除成功'
+		})
+		setTimeout(() => {
+			editId.value = null
+			uni.navigateBack({
+				delta: 1
+			})
+		}, 1000)
+	} catch (err) {
+		uni.showToast({
+			icon: 'none',
+			title: err.msg
+		})
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 53 - 21
pagesA/seenMe/index.vue

@@ -1,28 +1,26 @@
 <template>
-  <view class="defaultBgc" style="height: 100vh;">
-    <scroll-view class="scrollBox" scroll-y="true" @scrolltolower="loadingMore">
+  <view class="defaultBgc ss-p-x-20" style="height: 100vh; position: relative;">
+    <scroll-view v-if="showList" class="scrollBox" scroll-y="true" @scrolltolower="loadingMore">
       <view v-if="list.length > 0">
-        <view v-for="(item, index) in list" :key="index" class="ss-m-t-20" @click="toDetail(item)">
+        <view v-for="(item, index) in list" :key="index" class="ss-m-t-20" @click="jumpToEnterpriseDetail(item.enterprise.id)">
           <view class="sub-li-bottom">
             <view class="avatarBox">
               <image class="r-avatar" :src="getUserAvatar(item.contact.avatar, item.contact.sex)"></image>
             </view>
             <view class="ss-m-l-30">
-              <span>{{ item.contact?.name || ' -- ' }}</span>
-              <span class="ss-m-x-10"> | </span>
-              <span>{{ item.post?.nameCn || '--' }}</span>
+              <span>{{ item.contact?.name }}</span>
+              <span class="ss-m-x-10" v-if="item.contact?.name && item.contact?.nameCn"> | </span>
+              <span>{{ item.post?.nameCn }}</span>
             </view>
           </view>
-          <view style="background-color: #fff;" class="ss-p-30">
+          <view style="background-color: #fff; border-radius: 0 0 12px 12px;" class="ss-p-30">
             <view class="d-flex align-center">
-              <image :src="item.enterprise.logoUrl" class="avatar" style="width: 60px; height: 60px;"></image>
+              <image :src="item.enterprise.logoUrl" style="width: 50px; height: 50px;"></image>
               <view style="flex: 1;" class="ss-m-l-30">
-                <view class="enterprise-name ellipsis">{{ item.enterprise.name }}</view>
+                <view class="enterprise-name ellipsis">{{ formatName(item.enterprise.anotherName || item.enterprise.name) }}</view>
                 <!-- 行业规模 -->
                 <view class="ss-m-y-15 font-size-12">
                   <span class="tag-gap color-666">
-                    <span>{{ item.enterprise.financingName }}</span>
-                    <span class="ss-m-x-10" v-if="item.enterprise.financingName && item.enterprise.industryName">|</span>
                     <span>{{item.enterprise.industryName }}</span>
                     <span class="ss-m-x-10" v-if="item.enterprise.industryName && item.enterprise.scaleName">|</span>
                     <span>{{item.enterprise.scaleName }}</span>
@@ -51,21 +49,36 @@
         <image src="https://minio.citupro.com/dev/static/nodata.png" mode="widthFix" style="width: 100vw;height: 100vh;"></image>
       </view>
     </scroll-view>
+	<view v-else class="noviewlist">
+		<view v-if="userInfo?.vipExpireDate > Date.now() && !userInfo?.entitlement?.viewersList">
+			当前会员套餐的权益不包含谁关注我,<span class="text-line" @tap.stop="handleToBuyVip">点击去升级</span>
+		</view>
+		<view v-if="!userInfo?.vipExpireDate || (userInfo?.vipExpireDate && userInfo?.vipExpireDate < Date.now())">
+			谁关注我为会员权益内容,<span class="text-line" @tap.stop="handleToBuyVip">点击去开通</span>
+		</view>
+	</view>
   </view>
 </template>
 
 <script setup>
-import { ref } from 'vue'
+import { ref, computed } from 'vue'
 import { getInterestedMePage } from '@/api/user'
-import { dealDictObjData } from '@/utils/position'
+import { dealDictObjData, jumpToEnterpriseDetail } from '@/utils/position'
 import { getUserAvatar } from '@/utils/avatar'
 import { timesTampChange } from '@/utils/date'
+import { formatName } from '@/utils/getText'
+import { userStore } from '@/store/user'
+import { onShow } from '@dcloudio/uni-app'
 
 const status = ref('more')
 const queryParams = ref({
   pageNo: 1,
   pageSize: 10
 })
+const useUserStore = userStore()
+const userInfo = computed(() => useUserStore?.userInfo)
+// 当前会员套餐是否可查看此模块
+const showList = computed(() => (new Date().getTime() < useUserStore?.userInfo?.vipExpireDate) && useUserStore?.userInfo?.entitlement?.viewersList)
 
 const list = ref([])
 const getList = async () => {
@@ -77,23 +90,26 @@ const getList = async () => {
     })
     list.value = list.value.concat(arr)
   }
-  status.value = arr?.length < queryParams.value.pageSize ? 'noMore' : 'more'
+  status.value = list.value?.length === +res.data.total ? 'noMore' : 'more'
 }
 
-getList()
+onShow(() => {
+	// 有会员权益能查看才请求接口
+	if (showList.value) getList()
+})
 
-// 加载跟多
+// 加载
 const loadingMore = () => { 
   status.value = 'loading'
   queryParams.value.pageNo++
   getList()
 }
 
-// 企业详情
-const toDetail = (item) => {
-  uni.navigateTo({
-    url: `/pagesB/companyDetail/index?id=${item.enterprise.id}`
-  })
+// 跳转会员套餐
+const handleToBuyVip = () => {
+	uni.navigateTo({
+		url: '/pagesA/vipPackage/index'
+	})
 }
 </script>
 
@@ -104,6 +120,7 @@ const toDetail = (item) => {
   background: linear-gradient(90deg, #f5fcfc 0, #fcfbfa 100%);
   font-size: 13px;
   padding: 5px 30rpx;
+  border-radius: 12px 12px 0 0;
   .avatarBox {
     max-width: 40px;
     max-height: 40px;
@@ -116,4 +133,19 @@ const toDetail = (item) => {
   width: 70vw;
   max-width: 70vw;
 }
+.noviewlist {
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	width: 98%;
+	text-align: center;
+	transform: translate(-50%, -50%);
+	color: #666;
+}
+.text-line {
+	color: #00B760;
+	font-weight: bold;
+	padding-bottom: 2px;
+	border-bottom: 1px solid #00B760;
+}
 </style>

+ 174 - 0
pagesA/student/addReport.vue

@@ -0,0 +1,174 @@
+<template>
+  <scroll-view class="scrollBox" scroll-y="true">
+    <view class="content">
+      <uni-forms
+        ref="infoRef"
+        v-model="formData"
+        :rules="formRules"
+        validateTrigger="bind"
+        label-width="86px"
+        labelAlign="right"
+      >
+        <uni-forms-item name="enterpriseId" label="实习企业" required>
+          <uni-data-picker v-model="formData.enterpriseId" :localdata="companyList" :clear-icon="false" popup-title="请选择实习企业" :clear="false" :map="{ text: 'enterpriseName', value: 'id' }"></uni-data-picker>
+        </uni-forms-item>
+        <uni-forms-item label="实习报告" name="report" class="f-straight">
+          <view style="display: flex;flex-wrap: wrap;">
+            <view v-for="(img, index) of reportList" :key="index">
+              <view class="upload-img" v-if="img">
+                <uni-icons size="35" type="clear" color="#fe574a" style="position: absolute;right: -15px; top: -15px; z-index: 9" @click="delReport(index)"></uni-icons>
+                <image :src="img" mode="contain" style="width: 200rpx;height: 200rpx;" @click="handlePreviewImage(index)"></image>
+              </view>
+            </view>
+            <view v-if="reportList?.length < 9" class="upload-file" @click="uploadPhotos">
+              <uni-icons type="plusempty" size="50" color="#f1f1f1"></uni-icons>
+            </view>
+          </view>
+        </uni-forms-item>
+        <view class="uploadTip text-center">*请上传实习报告图片(最多可上传9张图片)</view>
+        <view class="uploadTip text-center">*只支持JPG、JPEG、PNG类型的图片</view>
+      </uni-forms>
+    </view>
+    <view class="bottom-sticky">
+      <button type="primary" size="default" class="recomm-button" @click="submit">确认</button>
+    </view>
+  </scroll-view>
+</template>
+
+<script setup>
+import { ref, unref } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { uploadFile } from '@/api/file'
+import { saveStudentReport } from '@/api/student.js'
+
+const companyList = ref([])
+onLoad((options) => {
+  if (options.companyList) {
+    companyList.value = JSON.parse(options.companyList)
+  }
+})
+
+const formData = ref({
+  enterpriseId: null,
+})
+const formRules = {
+  enterpriseId: {
+		rules: [{required: true, errorMessage: '请选择实习企业' }]
+	},
+}
+
+const infoRef = ref()
+const submit = async () => {
+  const validate = await unref(infoRef).validate()
+  if (!validate) return uni.showToast({ title: '请选择企业', icon: 'none' })
+  if (!reportList.value?.length) return uni.showToast({ title: '请上传实习报告', icon: 'none' })
+  try {
+    await saveStudentReport({
+      ...formData.value,
+      urlList: reportList.value
+    })
+    uni.showToast({ title: '保存成功', icon: 'none' })
+    setTimeout(() => {
+      uni.navigateBack({
+        delta: 1
+      })
+    }, 500)
+  } catch (err) {
+    uni.showToast({ title: err?.msg || '保存失败', icon: 'none' })
+  }
+}
+
+const reportList = ref([])
+const delReport = (index) => {
+  reportList.value.splice(index, 1)
+}
+
+// 图片预览
+const handlePreviewImage = (index) => {
+  uni.previewImage({
+    current: 0,
+    urls: [reportList.value[index]]
+  })
+}
+// 上传
+const uploadPhotos = () => {
+  wx.chooseImage({
+    count: 1,
+    sizeType: ['original', 'compressed'],
+    sourceType: ['album', 'camera'],
+    success: function(res){
+      const size = res.tempFiles[0]?.size || 0
+      if (size >= 31457280) {
+        uni.showToast({
+          icon: 'none',
+          title: '头像上传大小不得超过 20MB !',
+          duration: 2000
+        })
+        return
+      }
+      const path = res.tempFilePaths[0]
+      uploadFile(path, 'img').then(res => {
+        formData.value.avatar = res.data
+        reportList.value.push(res.data)
+      }).catch(error => {
+        uni.showToast({
+          icon: 'error',
+          title: '图片上传失败!',
+          duration: 2000
+        })
+      })
+    }
+  })
+}
+
+</script>
+
+<style lang="scss" scoped>
+.img{
+  width: 28%;
+  height: 200rpx;
+  margin: 10rpx;
+  border: 1px solid #f4f4f4;
+}
+.wrapper{
+  height: 84vh;
+  overflow: auto;
+}
+:deep(.uni-section .uni-section-header__decoration) {
+  background-color: #00B760 !important;
+}
+.line {
+  border-top: 1px solid #ccc;
+}
+.picker {
+  flex: 1;
+  overflow: hidden;
+  margin-right: 12px;
+}
+.content {
+  padding: 20rpx;
+  padding-bottom: 50rpx;
+  .uploadTip {
+    color: #00B760;
+    margin-bottom: 20px;
+    font-size: 13px;
+  }
+}
+.upload-img{
+  position: relative;
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+}
+.upload-file{
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 10rpx;
+}
+</style>

+ 116 - 0
pagesA/student/certificateDetail-copy.vue

@@ -0,0 +1,116 @@
+<!-- 分享招聘会 -->
+<template>
+  <view style="position: relative;">
+    <view v-if="shareUrl" class="d-flex align-center flex-column justify-center" style="height: 100vh;">
+      <image v-if="!!shareUrl" :style="imgStyle" @click="handlePreviewSharePoster" :src="shareUrl" :show-menu-by-longpress="true"></image>
+      <view class="color-999 ss-m-t-20 font-size-14 ss-m-b-50">点击图片预览,长按图片保存</view>
+    </view>
+		<canvas canvas-id="posterCanvas" class="shareCanvas" :style="`width: ${canvasWidth}px; height: ${canvasHeight}px;`"></canvas>
+  </view>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { timesTampChange } from '@/utils/date'
+import { formatName } from '@/utils/getText'
+
+const canvasWidth = 500
+const canvasHeight = 700
+
+const shareUrl = ref('')
+const windowWidth = ref(0)
+
+const itemData = ref(null)
+onLoad(async (options) => {
+  if (options.itemData) {
+    itemData.value = JSON.parse(options.itemData)
+    createPoster()
+  }
+  const windowInfo = wx.getWindowInfo()
+  windowWidth.value = windowInfo.windowWidth
+})
+
+const imgStyle = computed(() => {
+  if (windowWidth.value <= 320) return `width: 259px; height: ${Math.round((canvasHeight*259)/canvasWidth)}px;`
+  if (windowWidth.value > 320 && windowWidth.value <= 375) return `width: 313px; height: ${Math.round((canvasHeight*313)/canvasWidth)}px;`
+  if (windowWidth.value > 375) return `width: 328px; height: ${Math.round((canvasHeight*328)/canvasWidth)}px;`
+})
+
+// 图片预览
+const handlePreviewSharePoster = () => {
+	uni.previewImage({
+		current: 0,
+		longPressActions: {
+			itemList: ['发送给朋友', '保存图片', '收藏']
+		},
+		urls: [shareUrl.value]
+	})
+}
+
+const getImageTempRatio = (url) => {
+  return new Promise((req, rej)=>{
+    wx.getImageInfo({
+      src:url,
+      success:(res) =>{
+        req(res)
+      }
+    })
+  })
+}
+
+const createPoster = async () => {
+  uni.showLoading({ title: '生成中...', mask: true })
+  var ctx = uni.createCanvasContext('posterCanvas')
+  //清空画布
+  ctx.clearRect(0, 0, canvasWidth, canvasHeight)
+
+  //背景图片
+  const { path: bgUrl } = await getImageTempRatio('https://minio.citupro.com/dev/static/bgc.jpg')
+  ctx.drawImage(bgUrl, 0, 0, canvasWidth, canvasHeight) // 路径、x、y、宽、高
+  
+  const info = { ...itemData.value }
+  // 绘制文字内容
+  ctx.setFontSize(16)
+  if (info?.student?.schoolName) ctx.fillText(info.student.schoolName, 50, 130)
+  if (info?.student?.majorName) ctx.fillText(info.student.majorName, 50, 160)
+  if (info?.person?.name) ctx.fillText(info.person.name, 50, 190)
+  if (info?.startTime) ctx.fillText(timesTampChange(info.startTime, 'Y-M-D'), 50, 250)
+  if (info?.endTime) ctx.fillText(timesTampChange(info?.endTime, 'Y-M-D'), 50, 310)
+  if (info?.enterprise?.anotherName || info?.enterprise?.name) ctx.fillText(formatName(info.enterprise?.anotherName || info.enterprise.name), 50, 400)
+  if (info?.evaluate) ctx.fillText(info.evaluate, 20, 460)
+  if (info?.createTime) ctx.fillText(timesTampChange(info?.createTime, 'Y-M-D'), 200, 520)
+
+  ctx.font = 'bold';
+  ctx.fillText('兹有', 20, 100)
+  ctx.fillText('同学于', 20, 220)
+  ctx.fillText('至', 20, 280)
+  ctx.fillText('在', 20, 340)
+  ctx.fillText('实习。', 20, 430)
+  ctx.fillText('特此证明。', 20, 490)
+
+  ctx.draw(false, () =>{
+    wx.canvasToTempFilePath({ 
+      canvasId: 'posterCanvas',
+      success:(res)=>{
+        shareUrl.value = res.tempFilePath
+        console.log('canvas-success', shareUrl.value)
+        uni.hideLoading({})
+      },
+      fail:(err)=>{
+        uni.hideLoading({})
+        console.log('canvas-fail', err)
+      }
+    })
+  })
+}
+</script>
+
+<style scoped lang="scss">
+.shareCanvas {
+	position: fixed;
+	top: -99999upx;
+	left: -99999upx;
+	z-index: -99999;
+}
+</style>

+ 174 - 0
pagesA/student/certificateDetail.vue

@@ -0,0 +1,174 @@
+<template>
+	<view v-if="itemData" style="width: 100vw;height: 100vh;overflow: auto;">
+		<view class="cer-img">
+      <image src="https://minio.citupro.com/dev/static/bgc.jpg" mode="scaleToFill" style="width: 100%;height: 100%;"></image>
+      <view class="cer-introduce">
+				兹有
+				<span class="cer-text">{{ itemData?.student?.schoolName }}</span>
+        <span class="cer-text">{{ itemData?.student?.majorName }}</span>
+        专业<span class="cer-text">{{ itemData?.person?.name }}</span>
+        同学于<span class="cer-text">{{ itemData?.startTime ? timesTampChange(itemData?.startTime, 'Y-M-D') : '' }}</span>
+        至<span class="cer-text">{{ itemData?.endTime ? timesTampChange(itemData?.endTime, 'Y-M-D') : '' }}</span>
+        在<span class="cer-text">{{ formatName(itemData?.enterprise?.anotherName || itemData?.enterprise?.name) }}</span>
+        实习。
+			</view>
+			<view class="cer-comment">{{ itemData?.evaluate }}</view>
+			<view class="cer-prove">特此证明。</view>
+			<view class="cer-end">
+        <view>{{ itemData?.createTime ? timesTampChange(itemData?.createTime, 'Y-M-D') : '' }}</view>
+      </view>
+		</view>
+
+		<view class="bottom-sticky">
+      <view style="display: flex;justify-content: space-evenly;align-itens: center;width: 100%;margin: 20rpx 0;">
+        <!-- <view @click="shareClick" style="display: flex;justify-content: center;flex-direction: column;align-items: center;">
+          <uni-icons type="redo-filled" size="24" color="#00B760"/>
+          <span style="color:#00B760;font-weight:bold;">分享</span>
+        </view> -->
+        <button v-if="itemData?.certificate" type="primary" size="default" class="buttons" @click="viewReport(itemData)">查看附件</button>
+        <button v-else type="primary" size="default" class="buttons" style="background-color:grey;">查看附件</button>
+      </view>
+    </view>
+	</view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { preview } from '@/utils/preview'
+import { onLoad } from '@dcloudio/uni-app'
+import { timesTampChange } from '@/utils/date'
+import { formatName } from '@/utils/getText'
+
+const itemData = ref(null)
+onLoad((options) => {
+  if (options.itemData) itemData.value = JSON.parse(options.itemData)
+})
+
+const viewReport = (item) => {
+	if (!item.certificate) {
+		uni.showToast({
+      title: '加载失败,请稍后重试',
+      icon: 'none',
+      duration: 2000
+    })
+		return
+	}
+	preview(item.certificate)
+}
+
+// 分享按钮点击事件
+// const shareClick = () => {
+// }
+
+</script>
+
+<style lang="less" scoped>
+.cer-img{
+  width: 100vw;
+  height: 89.5vh;
+  position: relative;
+  font-weight: 600;
+}
+.cer-introduce{
+  width: 70%;
+  position: absolute;
+  top: 51%;
+  left: 50%;
+  transform: translate(-50%,-50%);
+  text-indent: 2em;
+	font-weight: 500;
+}
+.cer-text{
+  text-decoration: underline;
+  margin: 0 3px;
+	font-weight: 700;
+}
+.cer-comment{
+	width: 70%;
+  position: absolute;
+  top: 68%;
+  left: 50%;
+  transform: translate(-50%,-50%);
+	text-indent: 2em;
+	display: -webkit-box;
+	-webkit-box-orient: vertical;
+	-webkit-line-clamp: 3;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	font-weight: 500;
+}
+.cer-prove{
+  width: 70%;
+  position: absolute;
+  top: 82%;
+  left: 50%;
+  transform: translate(-50%,-50%);
+  text-indent: 2em;
+	font-weight: 500;
+}
+.cer-end{
+  position: absolute;
+  top: 87%;
+  right: 16%;
+	font-weight: 500;
+}
+button::after{
+	border:none;
+}
+.pop-btn-wrapper{
+	position: relative;
+	margin-top: 15vh;
+	.pop-confirm{
+		background-color: #fff;
+		color: #4b9afd;
+		font-size: 21px;
+		width: 45vw;
+		position: absolute;
+		bottom: 0;
+		left: 0;
+	}
+	.pop-cancel{
+		background-color: #fff;
+		font-size: 21px;
+		width: 45vw;
+		position: absolute;
+		bottom: 0;
+		right: 0;
+	}
+}
+.buttons{
+  width: 70vw;
+  height: 44px;
+  border-radius: 25px;
+  background-color: #00B760;
+  margin: 0;
+}
+
+.share-pop {
+  width: 100%;
+  // height:300rpx;
+  display: flex;
+  justify-content: center;
+  .f-straight {
+    margin: 40rpx;
+    background: unset;
+    &::after{
+      border:none !important;
+    }
+  }
+  .share-round {
+    border-radius:50%;
+    height:100rpx;
+    width:100rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  .share-round-1 {
+    background-color:#22a039;
+  }
+  .share-round-2 {
+    background-color:#3693cd;
+  }
+}
+</style>

+ 116 - 0
pagesA/student/certificateDetailCanvas.vue

@@ -0,0 +1,116 @@
+<!-- 分享招聘会 -->
+<template>
+  <view style="position: relative;">
+    <view v-if="shareUrl" class="d-flex align-center flex-column justify-center" style="height: 100vh;">
+      <image v-if="!!shareUrl" :style="imgStyle" @click="handlePreviewSharePoster" :src="shareUrl" :show-menu-by-longpress="true"></image>
+      <view class="color-999 ss-m-t-20 font-size-14 ss-m-b-50">点击图片预览,长按图片保存</view>
+    </view>
+		<canvas canvas-id="posterCanvas" class="shareCanvas" :style="`width: ${canvasWidth}px; height: ${canvasHeight}px;`"></canvas>
+  </view>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { timesTampChange } from '@/utils/date'
+import { formatName } from '@/utils/getText'
+
+const canvasWidth = 500
+const canvasHeight = 700
+
+const shareUrl = ref('')
+const windowWidth = ref(0)
+
+const itemData = ref(null)
+onLoad(async (options) => {
+  if (options.itemData) {
+    itemData.value = JSON.parse(options.itemData)
+    createPoster()
+  }
+  const windowInfo = wx.getWindowInfo()
+  windowWidth.value = windowInfo.windowWidth
+})
+
+const imgStyle = computed(() => {
+  if (windowWidth.value <= 320) return `width: 259px; height: ${Math.round((canvasHeight*259)/canvasWidth)}px;`
+  if (windowWidth.value > 320 && windowWidth.value <= 375) return `width: 313px; height: ${Math.round((canvasHeight*313)/canvasWidth)}px;`
+  if (windowWidth.value > 375) return `width: 328px; height: ${Math.round((canvasHeight*328)/canvasWidth)}px;`
+})
+
+// 图片预览
+const handlePreviewSharePoster = () => {
+	uni.previewImage({
+		current: 0,
+		longPressActions: {
+			itemList: ['发送给朋友', '保存图片', '收藏']
+		},
+		urls: [shareUrl.value]
+	})
+}
+
+const getImageTempRatio = (url) => {
+  return new Promise((req, rej)=>{
+    wx.getImageInfo({
+      src:url,
+      success:(res) =>{
+        req(res)
+      }
+    })
+  })
+}
+
+const createPoster = async () => {
+  uni.showLoading({ title: '生成中...', mask: true })
+  var ctx = uni.createCanvasContext('posterCanvas')
+  //清空画布
+  ctx.clearRect(0, 0, canvasWidth, canvasHeight)
+
+  //背景图片
+  const { path: bgUrl } = await getImageTempRatio('https://minio.citupro.com/dev/static/bgc.jpg')
+  ctx.drawImage(bgUrl, 0, 0, canvasWidth, canvasHeight) // 路径、x、y、宽、高
+  
+  const info = { ...itemData.value }
+  // 绘制文字内容
+  ctx.setFontSize(16)
+  if (info?.student?.schoolName) ctx.fillText(info.student.schoolName, 50, 130)
+  if (info?.student?.majorName) ctx.fillText(info.student.majorName, 50, 160)
+  if (info?.person?.name) ctx.fillText(info.person.name, 50, 190)
+  if (info?.startTime) ctx.fillText(timesTampChange(info.startTime, 'Y-M-D'), 50, 250)
+  if (info?.endTime) ctx.fillText(timesTampChange(info?.endTime, 'Y-M-D'), 50, 310)
+  if (info?.enterprise?.anotherName || info?.enterprise?.name) ctx.fillText(formatName(info.enterprise?.anotherName || info.enterprise.name), 50, 400)
+  if (info?.evaluate) ctx.fillText(info.evaluate, 20, 460)
+  if (info?.createTime) ctx.fillText(timesTampChange(info?.createTime, 'Y-M-D'), 200, 520)
+
+  ctx.font = 'bold';
+  ctx.fillText('兹有', 20, 100)
+  ctx.fillText('同学于', 20, 220)
+  ctx.fillText('至', 20, 280)
+  ctx.fillText('在', 20, 340)
+  ctx.fillText('实习。', 20, 430)
+  ctx.fillText('特此证明。', 20, 490)
+
+  ctx.draw(false, () =>{
+    wx.canvasToTempFilePath({ 
+      canvasId: 'posterCanvas',
+      success:(res)=>{
+        shareUrl.value = res.tempFilePath
+        console.log('canvas-success', shareUrl.value)
+        uni.hideLoading({})
+      },
+      fail:(err)=>{
+        uni.hideLoading({})
+        console.log('canvas-fail', err)
+      }
+    })
+  })
+}
+</script>
+
+<style scoped lang="scss">
+.shareCanvas {
+	position: fixed;
+	top: -99999upx;
+	left: -99999upx;
+	z-index: -99999;
+}
+</style>

+ 73 - 0
pagesA/student/enterpriseRecommendationLetter.vue

@@ -0,0 +1,73 @@
+<template>
+  <view style="height: 98vh; background-color: #f2f4f7; padding-top: 10px;">
+    <view v-if="list.length > 0">
+      <uni-card v-for="(item, index) in list" class="list-item" @tap.stop="viewReport(item)" :key="index" :is-shadow="true" :border='false' shadow="0px 0px 3px 1px rgba(0,0,0,0.1)">
+        <view class="font-weight-bold">From: {{ formatName(item.enterprise.enterpriseName) }}</view>
+				<view>创建时间:{{ timesTampChange(item.entity.createDate) }}</view>
+        <view style="text-align: end;">
+          <text class="color-primary" @tap.stop="viewReport(item)">点击查看</text>
+        </view>
+      </uni-card>
+      <uni-load-more status="noMore" style="line-height: 20vh;" />
+		</view>
+		<view v-else class="nodata-img-parent">
+			<image src="https://minio.citupro.com/dev/static/nodata.png" mode="widthFix" style="width: 100vw;height: 100vh;"></image>
+		</view>
+  </view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { getRecommendationPage } from '@/api/student'
+import { onShow } from '@dcloudio/uni-app'
+import { formatName } from '@/utils/getText'
+import { timesTampChange } from '@/utils/date'
+import { preview } from '@/utils/preview'
+
+const list = ref([
+  {
+    enterprise: {
+      enterpriseName: '门墩儿科技有限公司',
+    },
+    entity: {
+      createDate: 1740570206120,
+      fileUrl: 'https://minio.menduner.com/dev/person/725759784858554368/attachment/7cde29dc69c1403649be55d4c2bfd3d8304c088dc79ab25afe9c4bf55d3b382f.docx'
+    }
+  }
+])
+// list.value = Array.from({ length: 10 }, () => list.value[0]);
+
+// 推荐信预览
+const viewReport = (item) => {
+  if (!item.entity.fileUrl) {
+    uni.showToast({
+      title: '加载失败,请稍后重试',
+      icon: 'none',
+      duration: 2000
+    })
+    return
+  }
+  preview(item.entity.fileUrl)
+}
+
+// 获取推荐信列表
+const getList = async () => {
+  try {
+    const { data } = await getRecommendationPage({ size: 9999, current: 1 })
+    console.log(data, '推荐信列表')
+    // list.value = data.records.reverse()
+  } catch {}
+}
+
+onShow(async () => {
+  await getList()
+})
+</script>
+
+<style lang="scss" scoped>
+.list-item {
+  background-color: #fff;
+  border-radius: 3px;
+  box-shadow: 0px 0px 3px 1px rgba(0,0,0,0.1);
+}
+</style>

+ 45 - 0
pagesA/student/index.vue

@@ -0,0 +1,45 @@
+<template>
+  <view class="card">
+    <uni-list>
+      <uni-list-item
+        v-for="item in list"
+        :clickable="true"
+        :key="item.title"
+        :title="item.title"
+        :showArrow="item.hideArrow ? false : item.rightTex === '未开放' ? false : true"
+        :rightText="item.rightTex || ''"
+        @click="handleToLink(item)"
+      >
+      </uni-list-item>
+    </uni-list>
+  </view>
+</template>
+<script setup>
+import { ref } from 'vue'
+
+const defaultList = [
+	{	title: '学生信息',	path: '/pagesA/student/information'	},
+	{	title: '实习记录',	path: '/pagesA/student/internshipRecord' },
+	{	title: '实习报告',	path: '/pagesA/student/internshipReport' },
+	// {	title: '实习证书',	path: '/pagesA/student/internshipCertificate'	},
+	// {	title: '企业推荐信',	path: '/pagesA/student/enterpriseRecommendationLetter' },
+	{	title: '实习管家',	path: '/pagesA/student/internshipButler' },
+]
+const list = ref(defaultList.filter(e => !e.hide))
+
+const handleToLink = (item) => {
+  if (item.rightTex === '未开放') return
+	if (item.path) return uni.navigateTo({ url: item.path })
+}
+
+</script>
+<style lang="scss" scoped>
+:deep(.uni-list-item) {
+	height: 120rpx !important;
+	line-height: 120rpx !important;
+}
+:deep(.uni-list-item__content-title) {
+	font-size: 32rpx !important;
+	font-weight: 500;
+}
+</style>

+ 207 - 0
pagesA/student/information.vue

@@ -0,0 +1,207 @@
+<!-- 校验是否完善人才必填信息 -->
+<template>
+  <scroll-view class="scrollBox" scroll-y="true">
+    <view class="content">
+      <!-- <view class="text-center ss-m-b-50 font-size-20 color-primary">学生信息认证</view> -->
+      <uni-forms
+        ref="baseInfoRef"
+        v-model="formData"
+        :rules="formRules"
+        validateTrigger="bind"
+        label-width="131px"
+        labelAlign="right"
+      >
+        <uni-forms-item name="schoolId" label="就读学校" required>
+          <uni-data-picker v-model="formData.schoolId" :localdata="selects?.schools" :clear-icon="false" popup-title="请选择就读学校" @change="getSelectData(0)" :map="{ text: 'name', value: 'schoolId' }"></uni-data-picker>
+        </uni-forms-item>
+        <uni-forms-item name="schoolDeptId" label="所在院系" required>
+          <uni-data-picker v-model="formData.schoolDeptId" :localdata="selects?.dept" :clear-icon="false" popup-title="请选择所在院系" @change="getSelectData(2)" :map="{ text: 'name', value: 'id' }"></uni-data-picker>
+        </uni-forms-item>
+        <uni-forms-item name="majorId" label="所学专业" required>
+          <uni-data-picker v-model="formData.majorId" :localdata="selects?.major" :clear-icon="false" popup-title="请选择所学专业" @change="getSelectData(2)" :map="{ text: 'nameCn', value: 'id' }"></uni-data-picker>
+        </uni-forms-item>
+        <uni-forms-item name="schoolClassId" label="所在班级" required>
+          <searchComBox ref="schoolClassIdRef" v-model="formData.schoolClassId" :candidates="classList" itemTextName='schoolClassName' itemValueName='schoolClassId' labelKey='name' valueKey='id' placeholder="请选择所在班级"></searchComBox>
+        </uni-forms-item>
+        <uni-forms-item name="studentNo" label="学号" required>
+          <uni-easyinput placeholder="请填写学号" v-model="formData.studentNo" :inputBorder="false" type="text"></uni-easyinput>
+        </uni-forms-item>
+        <uni-forms-item name="idCardNo" label="身份证号码" required>
+          <uni-easyinput placeholder="请输入身份证号码" v-model="formData.idCardNo" :inputBorder="false" type="text"></uni-easyinput>
+        </uni-forms-item>
+        <uni-forms-item name="emergencyContactName" label="紧急联系人姓名" required>
+          <uni-easyinput placeholder="请填写紧急联系人姓名" v-model="formData.emergencyContactName" :inputBorder="false" type="text"></uni-easyinput>
+        </uni-forms-item>
+        <uni-forms-item name="emergencyContactPhone" label="紧急联系人手机号" required>
+          <uni-easyinput placeholder="请填写紧急联系人手机号" v-model="formData.emergencyContactPhone" :inputBorder="false" type="number"></uni-easyinput>
+        </uni-forms-item>
+      </uni-forms>
+      <view class="f-horizon-center">
+        <button type="primary" size="default" class="send-button" @click="submit">保 存</button>
+      </view>
+    </view>
+  </scroll-view>
+</template>
+
+<script setup>
+import { ref, unref } from 'vue'
+import { mobile, isValidIdCard18 } from '@/utils/validate'
+import { saveStudentSimpleInfo, getStudentInfo } from '@/api/user'
+// import { userStore } from '@/store/user'; const useUserStore = userStore()
+import { getSchoolList, getDepartmentListBySchoolId, getMajorList } from '@/api/student'
+import searchComBox from '@/components/searchCombox'
+
+const baseInfoRef = ref()
+const formData = ref({ // 必填项目
+  schoolId: null,
+  schoolDeptId: null,
+  majorId: null,
+  schoolClassId: null,
+  schoolClassName: null,
+  studentNo: null,
+  idCardNo: null,
+  emergencyContactName: null,
+  emergencyContactPhone: null,
+})
+
+// 获取学生基本信息
+const studentInfoFun = async () => {
+  const { data } = await getStudentInfo()
+  // 回显
+  Object.keys(data).length && Object.keys(formData.value).length && Object.keys(formData.value).forEach(key => formData.value[key] = data[key])
+  // if (!formData.value?.schoolClassId && formData.value?.schoolClassName) formData.value.schoolClassId = formData.value.schoolClassName
+  await getSelectData('default', true)
+  await getSelectData(0, true)
+  await getSelectData(1, true)
+  await getSelectData(2, true)
+}
+studentInfoFun()
+
+// // 下拉列表 
+const selects = ref({})
+const classList = ref([])
+const getSelectData = async (type = 'default', init = false) => { // type: 0院系|1专业|2班级
+  const params = { ...(type !== 'default' && { type }) }
+  // 查院系用 schoolId 查班级用 parentId
+  if (type === 0) {
+    if (!formData.value?.schoolId) return
+    params.schoolId = formData.value.schoolId
+  }
+  if (type === 2) {
+    if (!formData.value?.schoolId && !formData.value?.schoolDeptId) return
+    if (formData.value?.schoolId) params.schoolId = formData.value.schoolId
+    if (formData.value?.schoolDeptId) params.parentId = formData.value.schoolDeptId
+  }
+  const api = {
+    default: getSchoolList,
+    0: getDepartmentListBySchoolId,
+    1: getMajorList,
+    2: getDepartmentListBySchoolId,
+  }
+  const res = await api[type](params)
+  if (type === 'default') {
+    selects.value.schools = res?.data?.length ? res.data : []
+  }
+  if (type === 0) {
+    if (!init) {
+      formData.value.schoolDeptId = null
+      formData.value.schoolClassId = null
+    }
+    selects.value.dept = res?.data?.length ? res.data : []
+  }
+  if (type === 1) {
+    selects.value.major = res?.data?.length ? res.data : []
+  }
+  if (type === 2) {
+    if (!init) formData.value.schoolClassId = null
+    classList.value = res?.data?.length ? res.data : []
+    schoolClassIdRef.value && schoolClassIdRef.value.setLabel()
+  }
+}
+
+const formRules = {
+  idCardNo: isValidIdCard18,
+  phone: mobile,
+  schoolId:{
+		rules: [{required: true, errorMessage: '请选择就读学校' }]
+	},
+  schoolDeptId:{
+		rules: [{required: true, errorMessage: '请选择所在院系' }]
+	},
+  majorId: {
+		rules: [{required: true, errorMessage: '请填写所在专业' }]
+	},
+  schoolClassId: {
+		rules: [{required: true, errorMessage: '请填写所在班级' }]
+	},
+  studentNo: {
+		rules: [{required: true, errorMessage: '请填写学号' }]
+	},
+  emergencyContactName: {
+		rules: [{required: true, errorMessage: '请填写紧急联系人姓名' }]
+	},
+  emergencyContactPhone: mobile,
+}
+
+const schoolClassIdRef = ref()
+const submit = async () => {
+  const validate = await unref(baseInfoRef).validate()
+  if (!validate) return uni.showToast({ title: '请将信息补充完整', icon: 'none' })
+  let params = {...formData.value}
+
+  const schoolClassObj = schoolClassIdRef.value?.getValue()
+  params = { ...params, ...schoolClassObj }
+
+  try {
+    await saveStudentSimpleInfo(params)
+    uni.showToast({
+			icon: 'success',
+			title: '保存成功'
+		})
+		setTimeout(() => {
+      uni.navigateBack({
+        delta: 1
+      })
+    }, 1000)
+  } catch (err) {
+    uni.showToast({ title: err?.msg || '保存失败', icon: 'none' })
+  }
+}
+
+</script>
+
+<style lang="scss" scoped>
+.scrollBox {
+  width: 100vw;
+  // height: 100vh;
+  height: calc(100vh - 30rpx);
+  margin-bottom: 30rpx;
+}
+.content {
+  padding: 30rpx;
+}
+
+.changeRole {
+  color: var(--color-666);
+  font-size: 15px;
+  line-height: 26px;
+  margin-bottom: 40rpx;
+}
+.upload-img{
+  position: relative;
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+}
+.upload-file{
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 10rpx;
+}
+</style>

+ 31 - 0
pagesA/student/internshipButler.vue

@@ -0,0 +1,31 @@
+<template>
+    <view class="parents">
+        <image src="https://minio.citupro.com/dev/static/wx.jpg" mode="aspectFit" show-menu-by-longpress="true" class="img-code"></image>
+		<view class="tipsText">长按添加管家微信</view>
+    </view>
+</template>
+
+<script setup>
+	// 实习管家
+</script>
+
+<style lang="less" scoped>
+.parents{
+    position: relative;
+    background-color: #f8f8f8;
+}
+.img-code{
+    width: 100%;
+    height: 100vh;
+}
+.tipsText{
+    position: absolute;
+    top: 89%;
+    right: 50%;
+    transform: translate(50%, 0);
+    text-align: center;
+    color: #c2c5c7;
+    font-size: 12px;
+    font-weight: 700;
+}
+</style>

+ 72 - 0
pagesA/student/internshipCertificate.vue

@@ -0,0 +1,72 @@
+<template>
+  <view style="height: 98vh; background-color: #f2f4f7; padding-top: 10px;">
+    <view v-if="list.length > 0">
+      <uni-card v-for="(item, index) in list" class="list-item" @tap.stop="handleToDetail(item)" :key="index" :is-shadow="true" :border='false' shadow="0px 0px 3px 1px rgba(0,0,0,0.1)">
+        <view class="font-weight-bold">实习企业: {{ formatName(item.enterprise.enterpriseName) }}</view>
+				<view>创建时间:{{ timesTampChange(item.studentInternshipCertificate.createDate) }}</view>
+        <view>实习点评:{{ item.studentInternshipCertificate.comment }}</view>
+        <view style="text-align: end;">
+          <text class="color-primary" @tap.stop="handleToDetail(item)">点击查看</text>
+        </view>
+      </uni-card>
+		</view>
+		<view v-else class="nodata-img-parent">
+			<image src="https://minio.citupro.com/dev/static/nodata.png" mode="widthFix" style="width: 100vw;height: 100vh;"></image>
+		</view>
+  </view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { getEnterpriseCertificateList } from '@/api/student'
+import { onShow } from '@dcloudio/uni-app'
+import { formatName } from '@/utils/getText'
+import { timesTampChange } from '@/utils/date'
+import { preview } from '@/utils/preview'
+
+const list = ref([
+  {
+    enterprise: {
+      enterpriseName: '门墩儿科技有限公司',
+    },
+    entity: {
+      createDate: 1740570206120,
+      fileUrl: 'https://minio.menduner.com/dev/person/725759784858554368/attachment/7cde29dc69c1403649be55d4c2bfd3d8304c088dc79ab25afe9c4bf55d3b382f.docx'
+    },
+    studentInternshipCertificate: {
+      comment: '这是一条测试数据',
+      createDate: 1740570206120
+    }
+  }
+])
+
+// 查看证书详情
+const handleToDetail = (item) => {
+  console.log(111)
+  uni.navigateTo({
+    url: '/pagesA/student/certificateDetail?id=' + '1111'
+  })
+}
+
+// 获取实习证书列表
+const getList = async () => {
+  try {
+    const { data } = await getEnterpriseCertificateList({ size: 9999, current: 1 })
+    console.log(data, '实习证书')
+    // list.value = data.records.reverse()
+  } catch {}
+}
+
+onShow(async () => {
+  // await getList()
+})
+</script>
+
+<style lang="scss" scoped>
+.list-item {
+  background-color: #fff;
+  border-radius: 3px;
+  padding: 20px;
+  box-shadow: 0px 0px 3px 1px rgba(0,0,0,0.1);
+}
+</style>

+ 191 - 0
pagesA/student/internshipRecord.vue

@@ -0,0 +1,191 @@
+<template>
+  <view class="defaultBgc">
+    <uni-segmented-control :current="current" :values="controlList" @clickItem="handleChange" styleType="text" activeColor="#00B760" style="background-color: #fff;"></uni-segmented-control>
+    <scroll-view class="scrollBox defaultBgc" scroll-y="true" @scrolltolower="loadingMore" style="height: calc(100vh - 36px);">
+      <view v-if="dataList.length">
+        <uni-card v-for="(val, index) in dataList" :key="index" :is-shadow="true" :border='false' shadow="0px 0px 3px 1px rgba(0,0,0,0.1)">
+          <!-- 企业信息 -->
+          <view class="entInfoBox d-flex align-center ss-p-10 ss-p-l-20 ss-m-b-20">
+            <image class="enterAvatar" :src="val.enterprise.logoUrl ? val.enterprise.logoUrl : 'https://minio.citupro.com/dev/menduner/company-avatar.png'"></image>
+            <view class="ellipsis ss-m-l-20" style="flex: 1;">
+              <view class="enterpriseName font-size-16 ellipsis">{{ formatName(val.enterprise.anotherName || val.enterprise.name) }}</view>
+              <!-- <view class="ss-m-t-5">
+                <span class="color-999">{{ val.enterprise?.industryName || '' }}</span>
+                <span class="divider tag-gap1" v-if="val.enterprise?.industryName && val.enterprise?.scaleName"> | </span>
+                <span class="color-999">{{ val.enterprise?.scaleName || '' }}</span>
+              </view> -->
+            </view>
+          </view>
+          <!-- 职位信息 -->
+          <view class="list-shape ss-p-b-10" >
+            <view class="titleBox my-5">
+              <view class="job-name font-size-16" :style="{'max-width': !val.job.payFrom && !val.job.payTo ? '65vw' : '50vw'}">{{ formatName(val.job.name) }}</view>
+              <span v-if="!val.job.payFrom && !val.job.payTo" class="salary-text">面议</span>
+              <span v-else class="salary-text">{{ val.job.payFrom }}-{{ val.job.payTo }}{{ val.job.payName ? '/' + val.job.payName : '' }}</span>
+            </view>
+            <view style="font-size: 13px;" class="ss-m-t-10">
+              <span class="tag-gap" style="color: #808080;">
+                <span>{{ val.job.area?.str ?? '全国' }}</span>
+                <span class="ss-m-x-10" v-if="val.job.eduName">|</span>
+                <span>{{ val.job.eduName }}</span>
+                <span class="ss-m-x-10" v-if="val.job.expName">|</span>
+                <span>{{ val.job.expName }}</span>
+              </span>
+            </view>
+          </view>
+          <view>实习时间:{{ timesTampChange(val?.startTime, 'Y-M-D') }} 至 {{ timesTampChange(val?.endTime, 'Y-M-D') }}</view>
+          <view class="line ss-m-y-20"></view>
+          <view style="text-align: right;">
+            <button @tap="handleToReport(val)" class="ss-m-r-10" type="warning" size="mini" style="color:#fff; backgroundColor:#fb8c00;borderColor:#fb8c00">实习报告</button>
+            <button @tap="handleToCertificate(val)" class="ss-m-x-10" type="warning" size="mini" style="color:#fff; backgroundColor:#00b760;borderColor:#00b760">实习证书</button>
+            <button @tap="preview(val?.recommendationLetter)" class="ss-m-l-10" type="warning" size="mini" style="color:#fff; backgroundColor:#00897b;borderColor:#00897b">企业推荐信</button>
+          </view>
+        </uni-card>
+        <uni-load-more :status="more" />
+      </view>
+      <view v-else class="nodata-img-parent">
+        <image src="https://minio.citupro.com/dev/static/nodata.png" mode="widthFix" style="width: 100vw;height: 100vh;"></image>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { getDict } from '@/hooks/useDictionaries'
+import { getStudentPage } from '@/api/student'
+import { formatName } from '@/utils/getText'
+import { timesTampChange } from '@/utils/date'
+import { dealDictObjData } from '@/utils/position'
+import { preview } from '@/utils/preview'
+
+const more = ref('more')
+const current = ref(0)
+const query = ref({
+  pageNo: 1,
+  pageSize: 10
+})
+
+const dataList = ref([])
+const getData = async () => {
+  try {
+    const params = {
+      status: tabList.value[current.value]?.value,
+      ...query.value
+    }
+    if (!params?.status) return
+    const { data } = await getStudentPage(params)
+    const list = data?.list?.length && data.list || []
+
+    list.forEach(e => {
+      e.enterprise = dealDictObjData({}, e.enterprise)
+      e.job = dealDictObjData({}, e.job)
+    })
+    //
+    dataList.value = dataList.value.concat(list)
+    more.value = dataList.value?.length === data?.total ? 'noMore' : 'more'
+  } catch (error) {
+    query.pageNo--
+    more.value = 'more'
+  }
+}
+
+const handleChange = (e) => {
+  current.value = e.currentIndex
+  query.value.pageNo = 1
+  dataList.value = []
+  getData()
+}
+
+// 加载更多
+const loadingMore = () => {
+  more.value = 'loading'
+  query.value.pageNo++
+  getData()
+}
+
+const tabList = ref([])
+const controlList = ref([])
+const getTabList = async () => {
+  const { data } = await getDict('student_practice_status')
+  if (data.code !== 0) {
+    return
+  }
+  tabList.value = data?.data?.length && data.data || []
+  controlList.value = tabList.value.map(e => e.label) || []
+  if (tabList.value?.length) getData()
+}
+getTabList()
+
+// 实习报告
+const handleToReport = (val) => {
+  uni.navigateTo({ url: `/pagesA/student/internshipReport?enterpriseId=${val?.enterprise?.id}` })
+}
+
+// 查看证书详情
+const handleToCertificate = (val) => {
+  const itemData = JSON.stringify({
+    student: { schoolName: formatName(val?.student?.schoolInfo?.name), majorName: val?.student?.major?.nameCn },
+    person: { name: val?.person?.name },
+    enterprise: { anotherName: formatName(val?.enterprise?.anotherName || val?.enterprise?.name) },
+    startTime: val?.startTime,
+    endTime: val?.endTime,
+    evaluate: val?.evaluate,
+    certificate: val?.certificate,
+    createTime: val?.createTime
+  })
+  uni.navigateTo({ url: `/pagesA/student/certificateDetail?itemData=${itemData}` })
+}
+
+
+</script>
+
+<style scoped lang="scss">
+:deep(.segmented-control) {
+  background-color: #fff !important;
+}
+.enterpriseName {
+  color: #0E100F;
+  // font-weight: 700;
+}
+.enterAvatar {
+  width: 25px;
+  height: 25px;
+  // border-radius: 50%;
+  margin: auto;
+}
+.line {
+  border-top: 1px solid #ccc;
+}
+.list-shape {
+  background-color: #fff;
+  border-radius: 12px 12px 0 0;
+  .titleBox {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+}
+.salary-text {
+	float: right;
+  font-size: 15px;
+	color: #00B760;
+  font-weight: 700;
+}
+.job-name {
+  font-size: 30rpx;
+  // font-weight: 700;
+  color: #0E100F;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+.ellipsis {
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  overflow: hidden;
+}
+.entInfoBox {
+  background: linear-gradient(90deg, #f5fcfc, #fcfbfa);
+}
+</style>

+ 164 - 0
pagesA/student/internshipReport.vue

@@ -0,0 +1,164 @@
+<template>
+  <view style="padding: 20rpx 30rpx;">
+    <view class="d-flex align-center">
+      <uni-section title="企业:"></uni-section>
+      <uni-data-picker v-model="enterpriseId" class="picker" :localdata="companyList" placeholder="请选择要查看的企业" popup-title="选择企业" @change="getList" :map="{ text: 'enterpriseName', value: 'id' }"></uni-data-picker>
+      <button type="primary" size="mini" @click="getList" style="height: 35px; line-height: 35px; margin: 0;">刷新</button>
+    </view>
+    <view class="line ss-m-y-20"></view>
+    <view v-if="items.length > 0" class="wrapper">
+      <view v-for="(item,index) in items" :key="index" style="margin: 0 0 50rpx 0;">
+        <uni-section :title="item.date" type="line"></uni-section>
+        <view>
+          <image v-for="(url, i) in item.arr" :key="i" class="img" :src="url" mode="scaleToFill" @click="previewImage(item.arr,i)"></image>
+        </view>
+      </view>
+    </view>
+    <view v-else class="nodata-img-parent">
+      <image src="https://minio.citupro.com/dev/static/nodata.png" mode="widthFix" class="nodata-img-child"></image>
+    </view>
+    <view class="bottom-sticky">
+      <button type="primary" size="default" class="recomm-button" @click="addReport">新增实习报告</button>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { getStudentReportList, getStudentPracticeCompanyList } from '@/api/student.js'
+import { onLoad, onShow } from '@dcloudio/uni-app'
+import { formatName } from '@/utils/getText'
+
+const enterpriseId = ref(null)
+const items = ref([])
+const companyList = ref([])
+
+const getCompanyList = async () => {
+  try {
+    const { data } = await getStudentPracticeCompanyList()
+		data?.length && data.forEach(e => {
+			e.id = e.id.toString()
+			e.enterpriseName = formatName(e.anotherName || e.name)
+		})
+    companyList.value = data || []
+	} catch {}
+}
+
+// 实习报告列表
+const getList = async () => {
+	try {
+		const { data } = await getStudentReportList(enterpriseId.value ? { enterpriseId: enterpriseId.value } : {})
+    items.value = []
+		if (!data || !Object.keys(data).length) return
+		for (let item in data) {
+			items.value.push({ date: item, arr: data[item].map(e => e.url) })
+		}
+	} catch {}
+}
+
+// 预览图片
+const previewImage = (url, i) => {
+  uni.previewImage({
+    current: i,
+    urls: url,
+    longPressActions : {
+    itemList: ['发送给朋友', '保存图片', '收藏']
+    }
+  })
+}
+
+onShow(() => {
+  getCompanyList()
+  getList()
+})
+
+onLoad((options) => {
+  if (options.enterpriseId) {
+    enterpriseId.value = options.enterpriseId
+  }
+})
+
+const addReport = () => {
+  if (!companyList.value?.length) {
+    uni.showToast({
+			title: '没有查到实习企业记录!',
+			icon: 'none'
+		})
+    return
+  }
+  const list = JSON.stringify(companyList.value)
+  uni.navigateTo({ url: `/pagesA/student/addReport?companyList=${list}` })
+}
+</script>
+
+<style lang="scss" scoped>
+.img{
+  width: 28%;
+  height: 200rpx;
+  margin: 10rpx;
+  border: 1px solid #f4f4f4;
+}
+.wrapper{
+  height: 84vh;
+  overflow: auto;
+}
+:deep(.uni-section .uni-section-header__decoration) {
+  background-color: #00B760 !important;
+}
+.line {
+  border-top: 1px solid #ccc;
+}
+.picker {
+  flex: 1;
+  overflow: hidden;
+  margin-right: 12px;
+}
+.reportPopup {
+  width: 96vw;
+  .uploadTip {
+    color: #00B760;
+    margin-bottom: 20px;
+    font-size: 13px;
+  }
+  .dialog-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    color:#767a82;
+    padding: 20rpx;
+    .title {
+      font-weight: bold;
+      margin-left: 10rpx;
+    }
+  }
+  .dialog-content{
+    padding: 20rpx;
+    padding-bottom: 50rpx;
+  }
+  .dialog-bottom{
+    width: 100%;
+    height: 44px;
+    line-height: 44px;
+    text-align: center;
+    color: #fff !important;
+    background-color: #00B760 !important;
+  }
+}
+.upload-img{
+  position: relative;
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+}
+.upload-file{
+  width: 200rpx;
+  height: 200rpx;
+  border: 1px solid #f1f1f1;
+  margin: 10rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 10rpx;
+}
+</style>

+ 196 - 0
pagesA/vip/blockEnt/index.vue

@@ -0,0 +1,196 @@
+<!-- 屏蔽企业 -->
+<template>
+  <view style="height: 100%; overflow: auto;">
+    <!-- 搜索条 -->
+    <view class="white-bgc stick ss-p-t-10">
+      <view style="position: relative;">
+        <uni-search-bar
+          v-model="name"
+          placeholder="请输入公司关键字"
+          cancelButton="none"
+          :focus="false"
+          bgColor="#fff"
+          @confirm="getEntList($event.value)"
+          @clear="query.content = ''"
+        >
+        </uni-search-bar>
+        <button class="search-btn" @click.stop="getEntList" :loading="loading">搜索</button>
+      </view>
+    </view>
+    <!-- 已屏蔽的企业 -->
+    <view class="tags">
+      <view style="color: #777; width: 100%; margin-bottom: 15px;">已屏蔽的企业:</view>
+      <view
+        v-for="k in dataList" :key="k.id"
+        class="tag"
+        style="color: #00B760; background-color: #e2f0ef;;"
+        @tap="handleDel(k)"
+      >
+        {{ k.name }}
+        <uni-icons type="clear" size="16" color="#00B760"></uni-icons>
+      </view>
+    </view>
+     
+    <uni-popup ref="popup" type="bottom" background-color="#fff">
+      <view style="padding: 30rpx;">
+        <view class="entListBox">
+          <view style="color: #777; margin: 20rpx 0 30rpx 40rpx; ">请选择要屏蔽的企业</view>
+          <view v-if="!entList?.length" style="color: #777; text-align: center; margin-top: 20vh;">未查询到相关内容 . . .</view>
+          <uni-card v-else v-for="item in entList" :key="item.key" @click="joinBlock(item)" :is-shadow="true" :border='false' shadow="0px 0px 3px 1px rgba(0,0,0,0.1)">
+            <view>{{ item.value }}</view>
+          </uni-card>
+        </view>
+      </view>
+    </uni-popup>
+    <!-- 确认框 -->
+    <uni-popup ref="confirm" type="dialog">
+      <uni-popup-dialog
+        type="warn"
+        cancelText="取消"
+        confirmText="确认" 
+        title="系统提示"
+        :content="dialogContent"
+        @confirm="handleConfirm"
+        @close="handleClose"
+      ></uni-popup-dialog>
+    </uni-popup>
+  </view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { 
+  getBlockEnterpriseList,
+  handleBlockEnterprise,
+  handleUnBlockEnterprise,
+} from '@/api/vip'
+import { enterpriseSearchByName } from '@/api/resume.js'
+
+const dataList = ref([])
+const getData = async () => {
+  try {
+    const res = await getBlockEnterpriseList()
+    dataList.value = res?.data?.list || []
+  } catch (error) {
+    uni.showToast({
+      title: '查询数据失败,请重试',
+      icon: 'none'
+    })
+  }
+}
+getData()
+
+let confirm = ref()
+const handleConfirm = async () => {
+  if (isDel) {
+    await handleUnBlockEnterprise(enterpriseId)
+    uni.showToast({ title: '取消屏蔽成功!', icon: 'none' })
+  } else {
+    await handleBlockEnterprise({ enterpriseId })
+    uni.showToast({ title: '屏蔽企业成功!', icon: 'none' })
+  }
+  getData()
+}
+const handleClose = () => {
+  confirm.value.close()
+  dialogContent = ''
+  enterpriseId = ''
+}
+
+let enterpriseId = ''
+let dialogContent = ''
+let isDel = false
+
+// 屏蔽
+const joinBlock = (item) => {
+  isDel = false
+  enterpriseId = item.key
+  dialogContent = `是否屏蔽【${item.value}】?`
+  confirm.value.open()
+
+}
+// 取消屏蔽
+const handleDel = (item) => {
+  isDel = true
+  enterpriseId = item.id
+  dialogContent = `是否取消屏蔽【${item.name}】?`
+  confirm.value.open()
+}
+
+// 搜索企业
+const popup = ref()
+const loading = ref(false)
+const name = ref('')
+const entList = ref([])
+// 获取企业列表
+const getEntList = async () => {
+  if (!name.value || name.value === '公司' || name.value === '有限公司') {
+    uni.showToast({ title: '请输入公司名称关键字查询', icon: 'none' })
+    return
+  }
+  if (name.value.length < 2) {
+    uni.showToast({ title: '输入内容过少,请输入更多关键字查询', icon: 'none' })
+    return
+  }
+  try {
+    loading.value = true
+    const res = await enterpriseSearchByName({ name: name.value })
+    entList.value = res?.data || []
+    popup.value.open()
+    loading.value = false
+  } catch (error) {
+    uni.showToast({
+      title: '搜索失败',
+      icon: 'none'
+    })
+    loading.value = false
+  }
+}
+
+</script>
+<style lang="scss" scoped>
+.stick {
+  z-index: 1;
+  position: sticky;
+  top: 0;
+}
+.search-btn {
+  position: absolute;
+  right: 11px;
+  top: 10px;
+  width: 110px;
+  height: 40px;
+  font-size: 16px;
+  background-color: #00B760;
+  color: #fff;
+  border-radius: 0 5px 5px 0;
+  z-index: 9;
+}
+:deep(.uni-searchbar__box) {
+  width: calc(100% - 105px);
+  height: 40px !important;
+  border: 1px solid #00B760;
+  padding-right: 20px;
+  flex: none;
+}
+
+.tags {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: center;
+  padding: 30rpx;
+  .tag {
+    margin: 0 15rpx 12rpx 0;
+    border: 2rpx 15rpx #00B760;
+    color: #00B760;
+    white-space: nowrap;
+    padding: 4rpx 10rpx;
+    border-radius: 10rpx;
+    font-size: 24rpx;
+  }
+}
+.entListBox {
+  height: 70vh;
+  overflow: auto;
+}
+</style>

+ 191 - 0
pagesA/vip/index.vue

@@ -0,0 +1,191 @@
+<!-- vip权益 -->
+<template>
+  <view class="box">
+    <!-- vip信息 -->
+    <view class="vipBox">
+      <view class="avatar">
+        <img :src="getUserAvatar(baseInfo?.avatar, baseInfo?.sex)" alt="" class="img-box">
+        <image src="/static/svg/vip.svg" class="vipIcon"></image>
+      </view>
+      <view class="nameBox">
+				<view class="name font-weight-bold font-size-16">{{ baseInfo?.name || userInfo?.phone }}</view>
+				<view class="vipInfo font-size-14">
+          {{ pName }}将于{{ remaining }}后过期
+        </view>
+      </view>
+    </view>
+    <view style="height: 20rpx; background-color: #f8f8fa;"></view>
+    <view class="card">
+      <uni-list>
+        <uni-list-item
+          v-for="item in menuList"
+          :clickable="true"
+          :key="item.title"
+          :title="item.title"
+          :showArrow="!item.hideArrow"
+          :rightText="item.rightTex || ''"
+          @click="handleToLink(item)"
+        >
+        </uni-list-item>
+      </uni-list>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { userStore } from '@/store/user'
+import { ref, computed, watch } from 'vue'
+import { getUserAvatar } from '@/utils/avatar'
+import { getMembershipPackageList } from '@/api/vip'
+
+const useUserStore = userStore()
+const baseInfo = computed(() => useUserStore?.baseInfo)
+const userInfo = computed(() => useUserStore?.userInfo)
+
+
+let list = [
+	// {	title: '简历刷新次数', key: 'resumeRefreshCount', show: false,	path: '', hideArrow: true },
+	{	title: '简历模板', key: 'resumeTemplate', show: false,	path: '/pagesA/vip/template/index' },
+	{	title: '简历屏蔽', key: 'resumePrivacy', show: false,	path: '/pagesA/vip/blockEnt/index' },
+]
+const listKeys = list.map(e => e.key)
+
+const memberList = ref([])
+
+// 权益列表
+const menuList = ref([])
+watch(() => userInfo.value?.entitlement, 
+  (newVal) => {
+    // if (newVal) getPName()
+    if (newVal && listKeys?.length) {
+      listKeys.forEach(key => {
+        const item = list.find(e => e.key === key)
+        if (!item) return
+        //
+        if (key === 'resumeRefreshCount') { // 简历刷新次数
+          item.rightTex = '剩余 ' + (newVal[key] ? newVal[key] : 0) + ' 次'
+        }
+        item.show = Boolean(newVal[key] || newVal[key] === 0)
+      })
+      menuList.value = list.filter(e => e.show)
+    }
+  },
+  {
+    // deep: true,
+    immediate: true
+  }
+)
+
+const remaining = computed(() => {
+  if (!userInfo.value?.vipExpireDate) return ' 0 天'
+  const diffInMs =  (userInfo.value?.vipExpireDate-0) - new Date().getTime()
+  const day = diffInMs / (1000 * 60 * 60 * 24)
+  return day < 1 ? '今天' : Math.floor(day) + '天'
+})
+
+
+// 列表跳转
+const handleToLink = (item) => {
+  if (item?.path) {
+    uni.navigateTo({
+      url: item.path
+    })
+  }
+}
+const pName = ref('')
+let getPNameNum = 0 
+const getPName = () => {
+  if (!userInfo.value?.vipFlag) {
+    getPNameNum++
+    if (getPNameNum > 6)
+    setTimeout(() => { getPName() }, 1000);
+    return
+  }
+  memberList.value.forEach(e => {
+    if (e.id && e.id.toString() === userInfo.value?.vipFlag?.toString()) pName.value = e.name
+  })
+}
+
+const getMemberList = async () => {
+  try {
+    const { data } = await getMembershipPackageList()
+    if (!data || data.length === 0) {
+      return
+    }
+    memberList.value = data
+    // let vipFlagIndex = null
+    // const list = data.map((item, index) => {
+    //   item.id = item.id?.toString()
+    //   if (item.id === userInfo.value?.vipFlag) vipFlagIndex = index // 低于当前套餐的(套餐)不展示
+    //   if (item.recommend) recommend.value = index // 推荐套餐
+    //   return {
+    //     ...item,
+    //     my: vipFlagIndex === index,
+    //     list: JSON.parse(item.text),
+    //     type: 3, // 订单类型 0平台订单|1求职端订单|2招聘端订单|3会员套餐
+    //     loading: false
+    //   }
+    // })
+    // // 低于当前套餐的(套餐)不展示
+    // memberList.value = vipFlagIndex ? list.slice(vipFlagIndex) : list
+    getPName()
+  } catch (error) {
+    uni.showToast({ title: '查询数据失败,请重试', icon: 'none' })
+  }
+}
+ 
+
+
+getMemberList()
+
+</script>
+<style lang="scss" scoped>
+.vipBox {
+	// color: #a18a0f;
+  padding: 80rpx 50rpx;
+  display: flex;
+  background: linear-gradient(121deg,#fde2c2 29.02%,#c19164 104.03%);
+  .avatar{
+    position: relative;
+    width: 100rpx;
+    height: 100rpx;
+    margin: 0;
+    .img-box {
+      width: 100%;
+      height: 100%;
+      border: 2rpx solid #ccc;
+      border-radius: 50%;
+      border: 1px solid gold;
+    }
+    .vipIcon {
+      position: absolute;
+      width: 50%;
+      height: 50%;
+      bottom: 0;
+      right: 0;
+      transform: translate(0, 30%);
+    }
+  }
+  .nameBox {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-around;
+    margin-left: 30rpx;
+    .name {
+      color: #724d2b;
+    }
+    .vipInfo {
+      color: #572a00;
+    }
+  }
+}
+
+:deep(.uni-list-item) {
+	height: 120rpx !important;
+	line-height: 120rpx !important;
+}
+:deep(.uni-list-item__content-title) {
+	font-size: 32rpx !important;
+	font-weight: 500;
+}
+</style>

+ 41 - 0
pagesA/vip/template/index.vue

@@ -0,0 +1,41 @@
+<!-- 简历模板 -->
+<template>
+  <view class="ss-p-b-100" style="height: 100vh; background-color: #f2f4f7;">
+    <view v-if="list.length > 0" class="ss-p-t-6">
+			<uni-card v-for="(item, index) in list" :key="index" :is-shadow="true" :border='false' shadow="0px 0px 3px 1px rgba(0,0,0,0.1)">
+				<view class="d-flex align-center">
+          <view @click="preview(item.url)"  style="flex: 1;">
+            <view class="font-size-14" style="font-weight: bold; margin: 15rpx 0; color: #777;">{{ item.title }}</view>
+          </view>
+          <uni-icons class="ss-m-l-20" color="#00B760" type="eye-filled" size="20"></uni-icons>
+          <uni-icons class="ss-m-l-20" color="#00B760" @click="null" type="download-filled" size="20"></uni-icons>
+        </view>
+			</uni-card>
+		</view>
+    <button class="send-button ss-m-b-30" @click="goto('/')">回到首页</button>
+  </view>
+</template>
+
+<script setup>
+import { preview } from '@/utils/preview'
+
+// 简历模板列表
+const list = [
+  { title: '创新精英简历模板', url: 'https://minio.menduner.com/dev/menduner/resumeTemplate/%E5%88%9B%E6%96%B0%E7%B2%BE%E8%8B%B1%E7%AE%80%E5%8E%86%E6%A8%A1%E6%9D%BF.docx' },
+  { title: '顶尖人才简历模板', url: 'https://minio.menduner.com/dev/menduner/resumeTemplate/%E9%A1%B6%E5%B0%96%E4%BA%BA%E6%89%8D%E7%AE%80%E5%8E%86%E6%A8%A1%E6%9D%BF.docx' },
+  { title: '高级职业经理人简历模板', url: 'https://minio.menduner.com/dev/menduner/resumeTemplate/%E9%AB%98%E7%BA%A7%E8%81%8C%E4%B8%9A%E7%BB%8F%E7%90%86%E4%BA%BA%E7%AE%80%E5%8E%86%E6%A8%A1%E6%9D%BF.docx' },
+  { title: '高效职场精英简历模板', url: 'https://minio.menduner.com/dev/menduner/resumeTemplate/%E9%AB%98%E6%95%88%E8%81%8C%E5%9C%BA%E7%B2%BE%E8%8B%B1%E7%AE%80%E5%8E%86%E6%A8%A1%E6%9D%BF.doc' },
+  { title: '领导力简历模板', url: 'https://minio.menduner.com/dev/menduner/resumeTemplate/%E9%A2%86%E5%AF%BC%E5%8A%9B%E7%AE%80%E5%8E%86%E6%A8%A1%E6%9D%BF.docx' },
+  { title: '行业精英专属简历模板', url: 'https://minio.menduner.com/dev/menduner/resumeTemplate/%E8%A1%8C%E4%B8%9A%E7%B2%BE%E8%8B%B1%E4%B8%93%E5%B1%9E%E7%AE%80%E5%8E%86%E6%A8%A1%E6%9D%BF.docx' },
+  { title: '行业领袖简历模板', url: 'https://minio.menduner.com/dev/menduner/resumeTemplate/%E8%A1%8C%E4%B8%9A%E9%A2%86%E8%A2%96%E7%AE%80%E5%8E%86%E6%A8%A1%E6%9D%BF.docx' },
+  { title: '专业卓越简历模板', url: 'https://minio.menduner.com/dev/menduner/resumeTemplate/%E4%B8%93%E4%B8%9A%E5%8D%93%E8%B6%8A%E7%AE%80%E5%8E%86%E6%A8%A1%E6%9D%BF.doc' },
+  { title: '卓越成就简历模板', url: 'https://minio.menduner.com/dev/menduner/resumeTemplate/%E5%8D%93%E8%B6%8A%E6%88%90%E5%B0%B1%E7%AE%80%E5%8E%86%E6%A8%A1%E6%9D%BF.docx' },
+  { title: '卓越职业轨迹简历模板', url: 'https://minio.menduner.com/dev/menduner/resumeTemplate/%E5%8D%93%E8%B6%8A%E8%81%8C%E4%B8%9A%E8%BD%A8%E8%BF%B9%E7%AE%80%E5%8E%86%E6%A8%A1%E6%9D%BF.docx' },
+]
+const goto = (url) => {
+  uni.switchTab({ url })
+}
+
+</script>
+<style lang="scss" scoped>
+</style>

+ 698 - 0
pagesA/vipPackage/index.vue

@@ -0,0 +1,698 @@
+<template>
+  <view>
+    <view class="vipBox">
+      <view class="avatar">
+        <img :src="getUserAvatar(baseInfo?.avatar, baseInfo?.sex)" alt="" class="img-box" :class="{'img-box-atc': userInfo?.vipExpireDate}">
+        <image v-if="new Date().getTime() < userInfo?.vipExpireDate" src="/static/svg/vip.svg" class="vipIcon"></image>
+      </view>
+      <view class="nameBox">
+        <view class="name font-weight-bold font-size-16">{{ baseInfo?.name || userInfo?.phone }}</view>
+        <view class="vipInfo font-size-14" v-if="new Date().getTime() < userInfo?.vipExpireDate">
+          {{ pName }}
+          <view>将于{{ remaining }}后过期</view>
+        </view>
+      </view>
+    </view>
+    <view>
+      <swiper class="swiper-box" :current="current" indicator-dots indicator-active-color="#f1b17a">
+        <swiper-item v-for="(item, index) in memberListLength" :key="index" class="swiper-items">
+          <view class="swiper-item" v-for="val in item" :key="val.id">
+            <view
+              class="card"
+              :class="{ recommend: val.recommend, vipFlag: val.my, active: val.id === chooseId}"
+              @tap="handleChoose(val)"
+            >
+              <text>{{ val.name }}</text>
+              <view>
+                <uni-icons color="#f30" type="icon-renminbi1688" size="16" custom-prefix="iconfont"></uni-icons>
+                <text>{{ val.price }}</text>
+              </view>
+            </view>
+          </view>
+        </swiper-item>	
+      </swiper>
+      <view v-if="typeof chooseId === 'number'" class="itemBox">
+        套餐权益 ( {{ list.name }} )
+        <uni-section
+          v-for="item in list.list"
+          :key="item.id"
+          class="item"
+          :class="{ active: item.active }"
+          titleColor="#774e20"
+          subTitleColor="#774e20"
+          :title="item.text"
+        >
+          <template v-slot:right>
+            <uni-icons color="#774e20" :type="item.active ? 'checkmarkempty' : 'closeempty'" size="20"/>
+          </template>
+        </uni-section>
+      </view>
+    </view>
+    <view class="pay" v-if="!list.my">
+      <view class="pay-box">
+        <view class="price">
+          <uni-icons color="#e68735" type="icon-renminbi1688" size="16" custom-prefix="iconfont"></uni-icons>
+          {{ amount }}
+        </view>
+        <view class="btn" @tap="handleOpen">
+          立刻升级
+        </view>
+        </view>
+    </view>
+    <uni-popup ref="popup" :is-mask-click="false" borderRadius="10px 10px 0 0" background-color="#eee">
+      <view class="popup-content">
+        <view class="popup-content-close">
+          <view class="icon" @tap="handleClose">
+            <uni-icons
+              type="closeempty"
+              color="#999"
+              size="24"
+            />
+          </view>
+        </view>
+        <view class="popup-content-main">
+          <view class="popup-content-main-count">
+            <view class="title">{{ list.name }} 充值</view>
+            <view class="pay">
+              <uni-icons color="#000" type="icon-renminbi1688" size="16" custom-prefix="iconfont"></uni-icons>
+              <view>{{ amount }}</view>
+            </view>
+          </view>
+          <view class="popup-content-main-type">
+            <view v-if="showPayMethods" class="card">
+              <radio-group @change="radioChange">
+                <label class="card-label" v-for="item in payTypeList" :key="item.value">
+                  <view class="name">
+                    <uni-icons :color="item.color" class="mr-1" :type="item.icon" size="24" custom-prefix="iconfont"></uni-icons>
+                    {{item.name}}
+                  </view>
+                  <view>
+                    <radio :value="item.value" :disabled="item.disabled" :checked="item.value === channelValue" />
+                  </view>
+                </label>
+              </radio-group>
+            </view>
+          </view>
+        </view>
+        <view class="popup-content-btn">
+          <button class="popup-content-btn-s" @tap="handlePay">
+            确认支付 
+            <uni-icons color="#FFF" type="icon-renminbi1688" size="16" custom-prefix="iconfont"></uni-icons>
+            {{ amount }}
+          </button>
+        </view>
+      </view>
+    </uni-popup>
+  </view>
+</template>
+
+<script setup>
+import { onHide, onShow } from '@dcloudio/uni-app';
+import { ref, computed } from 'vue'
+import { getUserAvatar } from '@/utils/avatar'
+import { userStore } from '@/store/user'
+import { getMembershipPackageList } from '@/api/vip'
+import { orderCreated, getOrder, getSocialUser, socialUserBind, payOrderSubmit, getOrderPayStatus, getEnableCodeList } from '@/api/common'
+const useUserStore = userStore()
+
+const baseInfo = computed(() => useUserStore?.baseInfo)
+// const userInfo = computed(() => useUserStore?.userInfo)
+const userInfo = ref(useUserStore?.userInfo || {})
+
+const memberList = ref([])
+const recommend = ref(null)
+const chooseId = ref(null)
+const orderInfo = ref(null)
+
+const popup = ref()
+
+const amount = computed(() => {
+  return parseFloat(+list.value.price).toFixed(2)
+})
+
+const memberListLength = computed(() => {
+  const result = [];  
+  for (let i = 0; i < memberList.value.length; i += 2) {  
+      const pair = memberList.value.slice(i, i + 2)
+      result.push(pair)
+  }
+  return result
+})
+
+const pName = computed(() => {
+  return memberList.value.find(item => +item.id === +userInfo.value?.vipFlag)?.name
+})
+
+const remaining = computed(() => {
+  if (!userInfo.value?.vipExpireDate) return null
+  const diffInMs =  userInfo.value?.vipExpireDate - new Date().getTime()
+  const day = diffInMs / (1000 * 60 * 60 * 24)
+  return day < 1 ? '今天' : Math.floor(day) + '天'
+})
+
+const list = computed(() => {
+  const item = memberList.value.find(item => item.id === chooseId.value)
+  return item ?? {}
+})
+
+const current = ref(0)
+const channelValue = ref('')
+const payType = [
+  {
+    name: '微信支付',
+    value: 'wx_lite',
+    icon: 'icon-weixinzhifu',
+    color: '#1AAD19'
+  },
+  {
+    name: '钱包支付',
+    value: 'wallet',
+    disabled: true,
+    icon: 'icon-qianbao1',
+    // color: '#fd9702'
+    color: '#00B760'
+  }
+]
+
+const radioChange = (e) => {
+  channelValue.value = e?.detail?.value || ''
+}
+
+const chooseItem = ref(null)
+const handleChoose = (val) => {
+  chooseId.value = val.id
+  chooseItem.value = val
+}
+
+const handleOpen = () => {
+  popup.value.open('bottom')
+}
+
+const handleClose = () => {
+  popup.value.close()
+}
+
+// 设置 openid 到本地存储,目前只有 pay 支付时会使用
+const setOpenid = (openid) => {
+  uni.setStorageSync('openid', openid)
+}
+
+const bind = () => {
+  return new Promise(async (resolve, reject) => {
+    // 1. 获得微信 code
+    const codeResult = await uni.login()
+    if (codeResult.errMsg !== 'login:ok') {
+      return resolve(false)
+    }
+    // 2. 绑定账号 // // 社交快捷登录
+    const obj = {
+      type: socialType,
+      code: codeResult.code,
+      state: 'default',
+    }
+    const bindResult = await socialUserBind(obj);
+    if (bindResult.code === 0) {
+      setOpenid(bindResult.data)
+      return resolve(true)
+    } else {
+      return resolve(false)
+    }
+  })
+}
+
+const bindWeiXin = () => {
+  uni.showModal({
+    title: '微信支付',
+    content: '请先绑定微信再使用微信支付',
+    success: function (res) {
+      if (res.confirm) {
+        // 微信小程序绑定
+        bind()
+      }
+    },
+  });
+}
+
+const socialType = 34; // 社交类型 - 微信小程序
+
+// 预支付
+const prepay = async (channel, orderData) => { 
+  return new Promise(async (resolve, reject) => {
+    let data = {
+      id: orderData.payOrder.id,
+      channelCode: channel,
+      channelExtras: {},
+    };
+    // 特殊逻辑:微信公众号、小程序支付时,必须传入 openid
+    if (['wx_pub', 'wx_lite'].includes(channel)) {
+      const userRes = await getSocialUser(socialType)
+      const openid = userRes?.data?.openid ? userRes.data.openid : null
+      // 如果获取不到 openid,微信无法发起支付,此时需要引导
+      if (!openid) {
+        bindWeiXin()
+        return
+      }
+      // console.log('openid:', openid)
+      data.channelExtras.openid = openid
+    }
+    // 发起预支付 API 调用
+    payOrderSubmit(data).then((res) => {
+      // 成功时
+      res.code === 0 && resolve(res)
+      // 失败时
+      if (res.code !== 0 && res.msg.indexOf('无效的openid') >= 0) {
+        // 特殊逻辑:微信公众号、小程序支付时,必须传入 openid 不正确的情况
+        if (
+          res.msg.indexOf('无效的openid') >= 0 || // 获取的 openid 不正确时,或者随便输入了个 openid
+          res.msg.indexOf('下单账号与支付账号不一致') >= 0
+        ) {
+          bindWeiXin()
+        }
+      }
+    })
+  })
+}
+
+let interTimer = null
+let payLoading = false
+const checkPayStatus = async (id) => {
+  if (!id) return
+  try {
+    if (payLoading || !interTimer) return
+    payLoading = true
+    const res = await getOrderPayStatus({ id })
+    if (res?.data?.status === 10) {
+      if (interTimer) clearInterval(interTimer)
+      // emit('paySuccess')
+      setTimeout(async () => {
+        const _userInfo = await useUserStore.getUserInfo()
+        userInfo.value = _userInfo
+        getMemberList() // 刷新套餐列表
+      }, 1500)
+    }
+  } catch (error) {
+    console.log(error)
+  } finally {
+    payLoading = false
+  }
+}
+
+// 计时器
+const initIntervalFun = () => {
+  if (interTimer) clearInterval(interTimer)
+
+  // 查询是否已经支付
+  const id = orderInfo.value?.payOrder?.id || orderInfo.value?.order?.payOrderId
+  if (id) {
+    interTimer = setInterval(() => {
+      checkPayStatus(id)
+    }, 1000)
+  }
+}
+
+const weChatMiniProgramPay = async (orderData) => {
+  orderInfo.value = orderData
+  let res = await prepay(channelValue.value, orderData); // 预支付
+  if (res?.code !== 0) {
+    return;
+  }
+  // 调用微信小程序支付
+  const payConfig = res?.data?.displayContent ? JSON.parse(res.data.displayContent) : null
+  if (!payConfig) return uni.showToast({ title: '购买失败', icon: 'none'})
+  uni.requestPayment({
+    provider: 'wxpay',
+    timeStamp: payConfig.timeStamp,
+    nonceStr: payConfig.nonceStr,
+    package: payConfig.packageValue,
+    signType: 'RSA',
+    paySign: payConfig.paySign,
+    success: (res) => {
+      initIntervalFun()
+      popup.value.close()
+      uni.showToast({ title: '支付成功', icon: 'none'})
+    },
+    fail: (err) => {
+      if (err.errMsg === 'requestPayment:fail cancel') {
+        uni.showToast({ title: '支付已取消', icon: 'none'})
+      } else {
+        // this.payResult('fail');
+        uni.showToast({ title: '支付失败', icon: 'none'})
+      }
+    },
+  });
+}
+
+// 支付
+const handlePay = async () => {
+  if (!channelValue.value) {
+    uni.showToast({ title: '请选择支付方式', icon: 'none'})
+    return
+  }
+  const val = chooseItem.value
+  try {
+    const res = await getOrder({
+      spuId: val.id, // 商品编号
+      type: val.type
+    })
+    if (res.data) {
+      // 获取支付码
+      weChatMiniProgramPay(res.data)
+      return
+    }
+
+    await orderCreated({
+      spuId: val.id, // 商品编号
+      spuName: val.name, // 商品名称
+      price: val.price*100, // 价格
+      type: val.type // 订单类型 0平台订单|1求职端订单|2招聘端订单|3会员套餐
+    })
+
+    const _res = await getOrder({
+      spuId: val.id, // 商品编号
+      type: val.type
+    })
+
+    // 获取支付码
+    weChatMiniProgramPay(_res.data)
+  } catch (error) {
+    console.log(error)
+  } finally {
+    val.loading = false
+  }
+}
+
+const getMemberList = async () => {
+  try {
+    const { data } = await getMembershipPackageList()
+    if (!data || data.length === 0) {
+      return
+    }
+    // memberList.value = data
+    let vipFlagIndex = null
+    const list = data.map((item, index) => {
+      if (new Date().getTime() < userInfo.value?.vipExpireDate && +item.id === +userInfo.value?.vipFlag) {
+        vipFlagIndex = index // 低于当前套餐的(套餐)不展示
+      }
+      if (item.recommend) {
+        recommend.value = index // 推荐套餐
+      }
+      return {
+        ...item,
+        price: item.price/100,
+        my: vipFlagIndex === index,
+        list: JSON.parse(item.text),
+        type: 3, // 订单类型 0平台订单|1求职端订单|2招聘端订单|3会员套餐
+        loading: false
+      }
+    })
+
+    memberList.value = vipFlagIndex ? list.slice(vipFlagIndex) : list // 低于当前套餐的(套餐)不展示
+    // 轮播current位置
+    // if ((!userInfo.value?.vipFlag || userInfo.value?.vipExpireDate - new Date().getTime() > 0 ) && typeof recommend.value === 'number') {
+    //   current.value = parseInt(recommend.value/2)
+    // }
+    handleChoose(memberList.value[0]) // 已购买或者未购买都选中第一个
+  } catch (error) {
+    uni.showToast({ title: '查询数据失败,请重试', icon: 'none' })
+  }
+}
+getMemberList()
+
+onShow(() => {
+  if (orderInfo && orderInfo.value?.id) initIntervalFun()
+})
+onHide(() => {
+  if (interTimer) clearInterval(interTimer)
+})
+
+// 余额和其他还没有接暂时只支持微信支付
+let payTypeList = []
+const showPayMethods = ref(false)
+// 获取支付方式
+const getPayMethodsList = async () => {
+  showPayMethods.value = false
+  payTypeList = []
+  try {
+    const res = await getEnableCodeList({appId: 14})
+    if (!res?.data?.length) {
+      return
+    }
+    payTypeList.push(...payType.filter(e => res.data.includes(e.value)))
+    const result = payType.find(item => !item.disabled && item.value)
+    if (result) channelValue.value = result.value
+    showPayMethods.value = true
+  } catch (error) {
+    console.log(error)
+  }
+}
+getPayMethodsList()
+
+</script>
+
+<style lang="scss" scoped>
+.vipBox {
+	// color: #a18a0f;
+  padding: 80rpx 50rpx;
+  display: flex;
+  background: linear-gradient(121deg,#fde2c2 29.02%,#c19164 104.03%);
+  .avatar{
+    position: relative;
+    width: 100rpx;
+    height: 100rpx;
+    margin: 0;
+    .img-box {
+      width: 100%;
+      height: 100%;
+      border: 2rpx solid #ccc;
+      border-radius: 50%;
+      border: 1px solid gold;
+    }
+    .img-box-atc {
+      border: 1px solid gold;
+    }
+    .vipIcon {
+      position: absolute;
+      width: 50%;
+      height: 50%;
+      bottom: 0;
+      right: 0;
+      transform: translate(0, 30%);
+    }
+  }
+  .nameBox {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-around;
+    margin-left: 30rpx;
+    .name {
+      color: #724d2b;
+    }
+    .vipInfo {
+      color: #572a00;
+    }
+  }
+}
+
+.swiper-box {
+  height: 230rpx;
+  .swiper-items {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+  }
+  .swiper-item {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    height: 200rpx;
+    padding: 20rpx 10rpx;
+    box-sizing: border-box;
+    .card {
+      color: #774e20;
+      background-color: rgb(255, 251, 248);
+      border: 1px solid #f1b17a;
+      width: 100%;
+      height: 100%;
+      border-radius: 10rpx;
+      padding: 0 20rpx;
+      box-sizing: border-box;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      position: relative;
+      overflow: hidden;
+      &.recommend {
+        &::after {
+          content: '推荐';
+          position: absolute;
+          right: 0;
+          top: 0;
+          padding: 6rpx 10rpx;
+          font-size: 28rpx;
+          background: linear-gradient(121deg,#fde2c2 29.02%,#c19164 104.03%);
+        }
+      }
+      &.vipFlag {
+        &::before {
+          content: '我的套餐';
+          position: absolute;
+          left: 0;
+          top: 0;
+          padding: 6rpx 10rpx;
+          font-size: 28rpx;
+          background: linear-gradient(121deg,#fde2c2 29.02%,#c19164 104.03%);
+        }
+      }
+      &.active {
+        box-shadow: 0 0 18rpx 0 rgb(216 160 82);
+      }
+    }
+  }
+}
+
+.itemBox {
+  padding: 20rpx 40rpx;
+  .item {
+    // padding: 10rpx 0;
+    margin-top: 20rpx;
+    // color: rgba(119,78,32,.5);
+    // &.active {
+      color:#774e20;
+    // }
+  }
+}
+
+.pay {
+  position: sticky;
+  bottom: 0;
+  padding: 0 40rpx 50rpx 40rpx;
+  box-sizing: border-box;
+  &-box {
+    width: 100%;
+    background: linear-gradient(121deg,#fde2c2 29.02%,#c19164 104.03%);
+    border-radius: 180rpx 0 180rpx 0;
+    box-shadow: 3rpx 6rpx 10rpx 0rpx rgb(216 160 82);
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    height: 100rpx;
+    .price {
+      padding: 0 40rpx;
+      font-size: 40rpx;
+      font-weight: 600;
+      color: #e68735;
+    }
+    .btn {
+      height: 100%;
+      display: flex;
+      align-items: center;
+      padding: 0 40rpx;
+      border: 2rpx solid #00B760;
+      background: #00B760;
+      color: #FFF;
+      border-radius: 180rpx 0 180rpx 0;
+      position: relative;
+      // &::after {
+      //   content: '';
+      //   position: absolute;
+      //   width: 50rpx;
+      //   height: 50rpx;
+      //   background: radial-gradient(top right, transparent 50%, #00B760 50%);
+      //   left: 0;
+      //   top: 0;
+      //   margin-left: -25rpx;
+      //   border-radius: 180rpx;
+      // }
+    }
+  }
+}
+
+.popup-content {
+  max-height: 500px;
+  display: flex;
+  flex-direction: column;
+  &-close {
+    display: flex;
+    padding: 10px;
+    justify-content: flex-end;
+    .icon {
+      width: 30px;
+      height: 30px;
+      background: #ccc;
+      border-radius: 30px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+  &-main {
+    flex: 1;
+    height: 0;
+    overflow-y: auto;
+    &-count {
+      margin-bottom: 20px;
+      text-align: center;
+      .title {
+        font-size: 28rpx;
+        color: #666
+      }
+      .pay {
+        font-size: 52rpx;
+        color: #000;
+        font-weight: 600;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        padding: 10px 0;
+      }
+    }
+    &-type {
+      width: 100%;
+      padding: 0 20px;
+      box-sizing: border-box;
+      .card {
+        border-radius: 10px;
+        margin: 0 auto;
+        background: #FFF;
+        padding: 10px;
+        &-label {
+          padding: 15px 0;
+          box-sizing: border-box;
+          display: flex;
+          justify-content: space-between;
+          border-bottom: 1px solid #eee;
+          &:last-of-type {
+            border-bottom: none;
+          }
+          .name {
+            display: flex;
+            align-items: center;
+            color: #333;
+          }
+        }
+      }
+    }
+  }
+  &-btn {
+    height: 70px;
+    width: 100%;
+    margin-top: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    &-s {
+      height: 40px;
+      width: 75%;
+      line-height: 40px;
+      color: #FFF;
+      // color: #724d2b;
+      background: #00B760;
+      // background: linear-gradient(121deg,#fde2c2 29.02%,#c19164 104.03%);
+      border-radius: 90px;
+    }
+  }
+}
+.mr-1 {
+  margin-right: 10px;
+}
+</style>

+ 174 - 0
pagesB/about/index.vue

@@ -0,0 +1,174 @@
+<template>
+  <view style="width: 100%; height: 100%;">
+    <view class="banner"></view>
+    <view class="white-bgc ss-p-x-20">
+      <view class="ss-p-b-30 ss-p-t-40">
+        <h1 class="title">公司简介</h1>
+        <p class="ss-m-b-10 ss-m-t-40 color-666">门墩儿是聚焦酒店及现代服务业人力资源技术与人才数据服务的互联网科技公司。</p>
+        <p class="color-666">通过一体化的人力资源运营技术平台,致力于为企业提供人力资源数据服务支持,同时基于数据与技术,为企业提供线上招聘、猎头、实习生及知识付费等全方位人力资源解决方案。</p>
+      </view>
+      <view class="d-flex box ss-m-t-30">
+        <view class="box-item" v-for="(val, i) in list" :key="i" :style="{'background-image': `url('${val.url}')`}">
+          <p class="ss-m-b-6">{{ val.title }}</p>
+          <p class="font-size-14 ss-p-t-6" style="border-top: 1px solid rgba(255, 255, 255, .5)">{{ val.desc }}</p>
+        </view>
+      </view>
+      <view class="ss-p-y-30">
+        <view class="line"></view>
+        <h1 class="title ss-m-t-30 ss-m-b-20">门墩儿合伙人</h1>
+        <view class="d-flex flex-column align-center">
+          <img src="https://minio.citupro.com/dev/menduner/consultant/simon.png" style="width: 230px; height: 280px;" alt="">
+          <p class="ss-m-t-4 ss-m-l-4 consultant-item__name">创始人 田森博士(Simon Tian)</p>
+        </view>
+        <img class="ss-m-t-80" src="https://minio.menduner.com/dev/menduner/team.jpg" alt="" style="width: 100%; height: 120px;">
+      </view>
+      <view class="ss-p-b-30">
+        <view class="line"></view>
+        <h1 class="title">学术和行业之声</h1>
+        <view class="ss-m-t-60">
+          <view v-for="(val, i) in introduce" :key="i" class="ss-p-y-30 bole-item">
+            <view class="left">
+              <img :src="val.avatar" style="width: 80px; height: 80px;" />
+            </view>
+            <view class="right">
+              <p class="font-size-18">{{ val.name }}</p>
+              <p class="font-size-14 ss-m-y-2">{{ val.job }}</p>
+              <p class="font-size-14">{{ val.company }}</p>
+              <p class="font-size-14 ss-m-t-10" v-html="val.desc"></p>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+const list = [
+  { title: '公司使命', desc: '成为中国酒店行业可信赖的人力资源科技公司。', url: 'https://minio.citupro.com/dev/menduner/home/introduct-bg-01.png' },
+  { title: '公司愿景', desc: '人力数据技术赋能人才发现,助力酒店企业提质增效,实现业务可持续成长。', url: 'https://minio.citupro.com/dev/menduner/home/introduct-bg-02.png' }
+]
+
+// 学术和行业之声
+const introduce = [
+  {
+    name: 'Professor Kaye Chon',
+    job: 'Dean and Chair Professor, and Walter Kwok Foundation Professor in International Hospitality Management,School of Hotel and Tourism Management',
+    company: 'The Hong Kong Polytechnic University',
+    avatar: 'https://minio.citupro.com/dev/menduner/home/about/%E5%9B%BE%E7%89%871.png',
+    desc: 'The unprecedented growth of hotel industry in China creates a huge demand for qualified workers for the industry. This platform is very much welcomed because it will provide a win-win solution to both the demand and supply side of the hospitality industry human capital.'
+  },
+  {
+    name: 'Benjamin Yan 严竹明',
+    job: 'Chairman',
+    company: 'Ruiyee Hospitality',
+    avatar: 'https://minio.citupro.com/dev/menduner/home/about/%E5%9B%BE%E7%89%872.png',
+    desc: '恭喜门墩儿成功上线。如何遴选优质酒店业人才一直是我们的一大难题,人才关乎企业未来,而门墩儿的人工智能、大数据等技术必将助我们一臂之力,用人问题从此迎刃而解。'
+  },
+  {
+    name: 'Thierry Brinte',
+    job: 'General Manager',
+    company: 'Gran Hotel Bristol by Kempinski',
+    avatar: 'https://minio.citupro.com/dev/menduner/home/about/%E5%9B%BE%E7%89%873.png',
+    desc: 'I wish to offer my congratulations to Simon and his teammates in launching this fully-dedicated talent career service and development social platform for the hotel industry in China. The platform will benefit both hotel professionals and hotel employers in their endeavors to find the right and qualified hotel talents to ensure successful hotel business operations.'
+  },
+  {
+    name: 'Emily Zhu 竺丽萍',
+    job: '总经理',
+    company: '杭州新侨饭店',
+    avatar: 'https://minio.citupro.com/dev/menduner/home/about/%E5%9B%BE%E7%89%874.png',
+    desc: '“有所为,有所不为”,只做专业领域,且只做专业领域的金字塔市场, 这是“门墩儿”平台的特点。既深谙买方市场的需求,又有卖方市场的广阔人脉,这是“门墩儿”平台的特长。“千军好买,一将难求”,作为酒店业主方,我们需要像“门墩儿”这样能提供“私人定制”服务的合作伙伴。我个人非常看好“门墩儿”平台的发展前景,Bravo !!'
+  },
+  {
+    name: 'Antony Platford',
+    job: 'Seasoned Hotelier',
+    company: 'Hotel and Tourism Consulting',
+    avatar: 'https://minio.citupro.com/dev/menduner/home/about/%E5%9B%BE%E7%89%875.png',
+    desc: 'I wish to congratulate Simon Tian on the establishment of Menduner. There is no doubt of the future success for this enterprise and its clients given the proven ability, <br/>skills and professional experience that Simon and his team can provide.'
+  },
+  {
+    name: '肖君 先生',
+    job: '高级运营副总裁',
+    company: '朗廷酒店集团中国区域',
+    avatar: 'https://minio.citupro.com/dev/menduner/home/about/%E5%9B%BE%E7%89%876.png',
+    desc: '门墩儿是专属于酒店行业的职业发展社交平台。希望能在这里携手更多志同道合的伙伴,<br/>追求我们共同的梦想。'
+  },
+  {
+    name: 'Nelson Li 李可',
+    job: '泛海酒店投资管理有限公司副总经理',
+    company: '武汉泛海费尔蒙酒店业主代表',
+    avatar: 'https://minio.citupro.com/dev/menduner/home/about/%E5%9B%BE%E7%89%877.png',
+    desc: '世界每时每刻都在发生变化,唯一不变的是我们对于这个行业的初心和热情。很高兴看到一群有梦想的酒店人为了遇见更好的自己,为了中国酒店业的未来勇敢的迈出了这一步…<br/>“精诚所至 金石为开”,愿“门墩儿”与中国酒店人共同成长,加油!'
+  },
+  {
+    name: 'Gerhard H. Zimmer',
+    job: 'President',
+    company: 'ZYG INC.',
+    avatar: 'https://minio.citupro.com/dev/menduner/home/about/%E5%9B%BE%E7%89%878.png',
+    desc: 'Simon, a very talented Hotelier who has made good use of his technical skills, combined with excellent human relations to provide a solid foundation for a most successful operation.'
+  },
+  {
+    name: 'Ann Leung 梁旭君',
+    job: '创始人',
+    company: '禾禾木文化发展有限公司',
+    avatar: 'https://minio.citupro.com/dev/menduner/home/about/%E5%9B%BE%E7%89%879.png',
+    desc: '找到对的门,认识对的人. 门墩儿,比酒店人更懂酒店人,从这里开启不同的圈层,<br/>打开职业生涯你不敢想象的可能!'
+  }
+]
+</script>
+
+<style scoped lang="scss">
+.line {
+  border-top: 1px solid #ccc;
+  width: 100%;
+  height: 1px;
+  margin: 30px 0;
+}
+.banner {
+  height: 150px;
+  background-image: url('https://minio.menduner.com/dev/menduner/about-banner.png');
+  background-position: center center;
+  background-repeat: no-repeat;
+  background-size: cover;
+}
+.title {
+  font-size: 19px;
+  font-weight: 400;
+  text-align: center;
+}
+.box {
+  width: 100%;
+  &-item {
+    width: 50%;
+    height: 120px;
+    border-radius: 8px;
+    background-position: center center;
+    background-repeat: no-repeat;
+    background-size: cover;
+    padding: 18px;
+    color: #fff;
+    &:nth-child(2n) {
+      margin-left: 40px;
+    }
+  }
+}
+.bole-item {
+  padding: 30px 0;
+  display: flex;
+}
+.left {
+  width: 90px;
+}
+.right {
+  font-size: 14px;
+  color: #666;
+  line-height: 2;
+  flex: 1;
+}
+.consultant-item__name {
+  font-family: FFScalaWebBold, Georgia, Utopia, Charter, serif;
+  font-weight: 700;
+  color: #4c4c4e;
+}
+</style>
+

+ 36 - 0
pagesB/agreement/CopyrightPolicy.vue

@@ -0,0 +1,36 @@
+<template>
+  <div class="conter">
+    <div class="Protocol">
+      <h1 class="segment">版权政策</h1>
+      <p class="text-color"><span class="text-font">苏州识喜识谊信息科技有限公司(以下称“门墩儿网站”、“本网站”)尊重他人的知识产权,志在提供内容不侵犯他人知识产权的网站。根据《中华人民共和国著作权法》、《信息网络传播权保护条例》、《互联网著作权行政保护办法》等相关法律、法规的规定,门墩儿网站针对网络侵权采取如下版权政策:</span></p>
+      <p class="text-color"><span class="text-font">1. 本网站对网络版权保护尽合理、审慎的义务,在有理由确信存在明显侵犯任何第三人版权的作品时,有权不事先通知并随时删除该涉嫌侵权作品;</span></p>
+      <p class="text-color"><span class="text-font">2. 本网站在接到符合法定要求的版权通知后,将迅速删除涉嫌侵权作品;</span></p>
+      <p class="text-color"><span class="text-font">3. 本网站采取必要的技术措施,尽可能防止相同的侵权作品的再次上传;</span></p>
+      <p class="text-color"><span class="text-font">4.在适当情况下,经本网站决定后,涉嫌一次或多次侵犯他人知识产权、或发布不准确或非法内容的会员或组织(视具体情况而定)的帐号将被禁用或关闭。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">版权侵犯通知</span></h2>
+      <p class="text-color"><span class="text-font">如果您认为门墩儿网站上提供的信息存储空间所上载、传播的任何内容侵犯了您的信息网络传播权或者删除、改变了您的权利管理电子信息的,您可以通过向本网站的邮箱发送邮件,或向本网站版权负责人寄送信件,要求本网站删除该作品或者断开该作品的链接。通知书需由权利人或其合法授权人亲笔签名,若为单位则需加盖单位公章。</span></p>
+      <p class="text-color"><span class="text-font">通知书应当包含下列内容:</span></p>
+      <p class="text-color"><span class="text-font">1. 权利人的姓名(名称)、联系方式、地址、身份证复印件(自然人)、单位登记证明复印件(单位);</span></p>
+      <p class="text-color"><span class="text-font">2. 要求删除或者断开链接的侵权作品的准确名称和网络地址,以便本网站能够发现并初步审核涉嫌侵权作品;</span></p>
+      <p class="text-color"><span class="text-font">3. 认为构成侵权的初步证明材料,包括但不限于对作品享有版权或依法享有信息网络传播权的权属证明等。</span></p>
+      <p class="text-color"><span class="text-font">您应对通知书的真实性负责。若通知书的内容不真实,您将承担由此造成的全部法律责任。因此建议在提交通知书前,先进行法律咨询。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">反驳通知</span></h2>
+      <p class="text-color"><span class="text-font">作品提供者收到本网站转送的通知书后,认为其提供的作品并未侵犯他人权利的,可向本网站提交反通知的书面说明,要求恢复被删除的作品或被断开的作品链接。反通知书需作品提供者或其合法授权人亲笔签名,若为单位则需加盖单位公章。</span></p>
+      <p class="text-color"><span class="text-font">反通知应当包含下列内容:</span></p>
+      <p class="text-color"><span class="text-font">1. 作品提供者的姓名(名称)、联系方式、地址、身份证复印件(自然人)、单位登记证明复印件(单位);</span></p>
+      <p class="text-color"><span class="text-font">2. 要求恢复被删除的作品,或者被断开链接的作品的准确名称和网络地址,以便本网站能够发现并初步审核涉嫌侵权的作品;</span></p>
+      <p class="text-color"><span class="text-font">3. 认为不构成侵权的初步证明材料,包括但不限于对作品享有著作权或依法享有信息网络传播权的权属证明等。</span></p>
+      <p class="text-color"><span class="text-font">作品提供者应对反通知书的真实性负责。若通知书的内容不真实,提供者将承担由此造成的全部法律责任。因此建议在提交反驳通知书前,先进行法律咨询。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">线下版权通知</span></h2>
+      <p class="text-color"><span class="text-font">投诉请邮寄至如下地址:</span></p>
+      <p class="text-color"><span class="text-font">地址:江苏省苏州工业园区林泉街 399 号东南大学国家 大学科技园(苏州)南工院(2#)304 室</span></p>
+      <p class="text-color"><span class="text-font">邮编:215123</span></p>
+      <p class="text-color"><span class="text-font">公司:苏州识喜识谊信息科技有限公司</span></p>
+      <p class="text-color"><span class="text-font">邮箱:services@menduner.com</span></p>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+@import '../../static/style/protocol/index.scss';
+</style>

+ 102 - 0
pagesB/agreement/UserBehaviorNorms.vue

@@ -0,0 +1,102 @@
+<template>
+  <div class="conter">
+    <div class="Protocol">
+      <h1 class="segment">用户行为规范</h1>
+      <h2 class="subtitle text-size"><span class="text-font">一、前言</span></h2>
+      <p class="text-color"><span class="text-font">《用户行为规范》(以下简称“本规范”)适用门墩儿招聘所有用户,是《门墩儿招聘用户服务协议》的重要组成部分,两者具有同等法律效力,用户同意或使用门墩儿招聘服务(即《门墩儿招聘用户协议》所提及的门墩儿招聘网页(www.menduner.com)、移动客户端(包括IOS、安卓及已有或未来将新增的任何其他移动客户端)等各类平台或媒介的服务,以下简称“本服务”)的行为即视为同意受到本规范的约束。本协议术语及定义与《门墩儿招聘用户协议》一致。
+        <strong>您应当仔细阅读并遵守本用户协议下的全部内容,特别是涉及免除或者限制我们的责任的条款,此类条款可能以黑体加粗的形式提示您注意。</strong></span></p>
+      <p class="text-color"><span class="text-font">本规范与《门墩儿招聘用户协议》不一致之处(如有),以本规范为准,本规范未提及事宜,以《门墩儿招聘用户协议》中的约定为准。</span></p>
+      <p class="text-color"><span class="text-font">用户(包括求职者用户和招聘者用户)在使用本服务(详情请查阅《门墩儿招聘用户协议》)的过程中,应当遵守宪法和其他适用的法律法规(以下简称“相关法律法规”),包括但不限于《中华人民共和国保守国家秘密法》、《中华人民共和国网络安全法》、《中华人民共和国数据安全法》、《中华人民共和国著作权法》、《中华人民共和国劳动法》、《中华人民共和国劳动合同法》、《中华人民共和国计算机信息系统安全保护条例》、《计算机软件保护条例》、《互联网电子公告服务管理规定》、《网络信息内容生态治理规定》、《信息网络传播权保护条例》、《互联网信息服务管理办法》、《网络信息内容生态治理规定》、《互联网用户账号名称管理规定》等有关计算机及互联网规定的法律法规,以及《中华人民共和国就业促进法》、《就业服务与就业管理规定》、《中华人民共和国广告法》、《中华人民共和国个人信息保护法》、《中华人民共和国民法典》、《互联网广告管理办法》、《网络招聘服务管理规定》等相关法律法规;应当遵循公序良俗,不得损害国家利益、公共利益和他人合法权益。</span></p>
+      <p class="text-color"><span class="text-font">您理解并同意,您须为您的门墩儿招聘账号(包括招聘方用户账号以及求职者用户账号)下的一切行为负责,包括您所发表的任何内容以及由此产生的任何后果。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">二、用户权利</span></h2>
+      <p class="text-color"><span class="text-font">用户在使用本服务期间,有权:</span></p>
+      <p class="text-color"><span class="text-font">1、根据《门墩儿招聘用户协议》和相关法律法规,制作、发布、上传由用户原创或取得了合法授权(含转授权)内容,包括但不限于文字、图片、语音、视频、直播等内容。</span></p>
+      <p class="text-color"><span class="text-font">2、根据《门墩儿招聘用户协议》,在我们的授权范围或门墩儿招聘各类平台或媒介支持的功能范围内,使用本服务。</span></p>
+      <p class="text-color"><span class="text-font">3、向我们提出与本服务有关的意见和建议,以帮助我们更好地向您提供服务。</span></p>
+      <p class="text-color"><span class="text-font">4、对其他用户的违法违规行为、违反本协议条款和规则的行为,与我们联系进行投诉和举报。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">三、用户义务</span></h2>
+      <p class="text-color"><span class="text-font">用户在使用门墩儿招聘服务期间,禁止:</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">1、制作、上传、复制、传送、传播包含下列内容的违法信息:</span></h2>
+      <p class="text-color"><span class="text-font">(1)反对宪法所确定的基本原则的;</span></p>
+      <p class="text-color"><span class="text-font">(2)危害国家安全,泄露国家秘密,颠覆国家政权,破坏国家统一的;</span></p>
+      <p class="text-color"><span class="text-font">(3)损害国家荣誉和利益的;</span></p>
+      <p class="text-color"><span class="text-font">(4)歪曲、丑化、亵渎、否定英雄烈士事迹和精神,以侮辱、诽谤或者其他方式侵害英雄烈士的姓名、肖像、名誉、荣誉的;</span></p>
+      <p class="text-color"><span class="text-font">(5)宣扬恐怖主义、极端主义或者煽动实施恐怖活动、极端主义活动的;</span></p>
+      <p class="text-color"><span class="text-font">(6)煽动民族仇恨、民族歧视,破坏民族团结的;</span></p>
+      <p class="text-color"><span class="text-font">(7)破坏国家宗教政策,宣扬邪教和封建迷信的;</span></p>
+      <p class="text-color"><span class="text-font">(8)散布谣言,扰乱经济秩序和社会秩序,破坏社会稳定的;</span></p>
+      <p class="text-color"><span class="text-font">(9)散布淫秽、色情、赌博、暴力、凶杀、恐怖或者教唆犯罪的;</span></p>
+      <p class="text-color"><span class="text-font">(10)侮辱或者诽谤他人,侵害他人名誉、隐私和其他合法权益的;</span></p>
+      <p class="text-color"><span class="text-font">(11)法律、行政法规禁止的其他内容。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">2、制作、上传、复制、传送、传播包含下列内容的不良信息:</span></h2>
+      <p class="text-color"><span class="text-font">(1)使用夸张标题,内容与标题严重不符的;</span></p>
+      <p class="text-color"><span class="text-font">(2)炒作绯闻、丑闻、劣迹等的;</span></p>
+      <p class="text-color"><span class="text-font">(3)不当评述自然灾害、重大事故等灾难的;</span></p>
+      <p class="text-color"><span class="text-font">(4)带有性暗示、性挑逗等易使人产生性联想的;</span></p>
+      <p class="text-color"><span class="text-font">(5)展现血腥、惊悚、残忍等致人身心不适的;</span></p>
+      <p class="text-color"><span class="text-font">(6)煽动人群歧视、地域歧视等的;</span></p>
+      <p class="text-color"><span class="text-font">(7)宣扬低俗、庸俗、媚俗内容的;</span></p>
+      <p class="text-color"><span class="text-font">(8)可能引发未成年人模仿不安全行为和违反社会公德行为、诱导未成年人不良嗜好等的;</span></p>
+      <p class="text-color"><span class="text-font">(9)其他对网络生态造成不良影响的内容。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">您知悉并同意,您不得向其他用户发布具有跟踪性、威胁性、伤害性、骚扰性或令人尴尬的内容;不得针对性、性别、年龄、体重、体形、残障、种族、宗教或性取向发表任何贬损性言论,亦不得支持针对任何个人或群体实施暴力行为,即使是以幽默的方式所表达,这包括针对任何群体或社区发表任何成见。在您参与线上沟通时,请围绕相关话题就事论事,您可以礼貌地对某条消息、帖子或某个话题表示反对,但请勿以嘲讽或侮辱的方式攻击其他用户。在您受到其他用户的攻击时,如果您反过来攻击对方,那么对方可能还会再次攻击您。</span></h2>
+      <p class="text-color"><span class="text-font">3、以任何方式危害未成年人,包括但不限于制作、上传、复制、传送、传播诱发未成年人模仿违反社会公德和违法犯罪行为的内容,含有恐怖、残酷等妨害未成年人身心健康的内容,含有披露未成年人个人隐私的内容。</span></p>
+      <p class="text-color"><span class="text-font">4、利用本服务从事危害网络安全的活动:</span></p>
+      <p class="text-color"><span class="text-font">(1)非法侵入门墩儿招聘或他人网络、干扰门墩儿招聘或他人网络正常功能、窃取网络数据等危害网络安全的活动。包括但不限于:出于任何目的,在未经授权的情况下使用、破坏或企图破坏、自动攻击、开发或滥用我们的资源或我们的网络安全保护措施,使用任何网络机器人、网络蜘蛛(SPIDER)、抓取工具、抓宠程序、拟人程序或其他非真实用户或避开、破坏技术措施等非正常的自动手段读取、复制、转存访问本服务的任何内容,未经门墩儿招聘许可使用插件、外挂或者通过其他第三方工具、运营平台或任何服务接入本服务和相关系统;对本服务的网络服务及相关软硬件设施进行破解、破坏、删除、修改或者增加,对计算机信息网络中存储或者传输的数据和应用程序进行删除、修改或者增加的;</span></p>
+      <p class="text-color"><span class="text-font">(2)上传、复制、传送、传播任何干扰、破坏或限制任何计算机软件、硬件或通讯设备功能的软件病毒或其他计算机代码、档案和程序之资料,干扰或破坏本服务或与提供本服务相连的服务器和网络;</span></p>
+      <p class="text-color"><span class="text-font">(3)向他人提供专门用于从事侵入网络、干扰网络正常功能及防护措施、窃取网络数据等危害网络安全活动的程序、工具。</span></p>
+      <p class="text-color"><span class="text-font">(4)明知他人从事危害网络安全的活动的,为其提供技术支持、广告推广、支付结算等帮助的行为。</span></p>
+      <p class="text-color"><span class="text-font">(5)其他违反法律法规,危害计算机网络安全的行为。</span></p>
+      <p class="text-color"><span class="text-font">5、以通过发布评论等任何方式向任何人发布任何垃圾信息。</span></p>
+      <p class="text-color"><span class="text-font">6、篡改其他用户发布的内容。</span></p>
+      <p class="text-color"><span class="text-font">7、任何未经授权的商业行为。包括但不限于:</span></p>
+      <p class="text-color"><span class="text-font">(1)对本服务的全部或任何部分,进行复制、拷贝、出售、转售或用于任何其他商业目的;</span></p>
+      <p class="text-color"><span class="text-font">(2)利用本服务进行任何牟利性经营活动等。</span></p>
+      <p class="text-color"><span class="text-font">(3)传播任何未经要求或授权的广告、宣传材料、“邮寄宣传片”、“垃圾邮件”、“连环信”、传销、违法直销或任何其他形式的此类兜售;</span></p>
+      <p class="text-color"><span class="text-font">(4)发布包含经我们自行判断后认为令人反感或者妨碍任何其他人士使用或享用本服务的内容;</span></p>
+      <p class="text-color"><span class="text-font">(5)发布可能会使我们或其他用户受到任何类型的任何伤害或承担任何责任的内容;</span></p>
+      <p class="text-color"><span class="text-font">(6)发布损害或降低与我们的标志有关的商誉的内容,包括但不限于:</span></p>
+      <p class="text-color"><span class="text-font">(7)强制、诱导其他用户关注、点击链接页面或分享信息;</span></p>
+      <p class="text-color"><span class="text-font">(8)未经门墩儿招聘书面许可利用门墩儿招聘账号和任何功能进行推广或互相推广的;</span></p>
+      <p class="text-color"><span class="text-font">(9)利用技术手段批量建立虚假账号。</span></p>
+      <p class="text-color"><span class="text-font">8、将无权传送的内容(例如受保密协议保护的保密资料、机密资料)、侵犯他人的个人信息、著作权、专利权、商标权、商业秘密或其他专属权利之内容、广告函件、促销资料、干扰、破坏或限制任何计算机软件、硬件或通讯设备功能的软件病毒或其他计算机代码、档案和程序之资料等作为任何内容进行上传、复制、传送、传播;</span></p>
+      <p class="text-color"><span class="text-font">9、从事侵害他人名誉权、肖像权、知识产权、商业秘密、隐私、个人信息等合法权益的行为。包括但不限于:</span></p>
+      <p class="text-color"><span class="text-font">(1)以任何不当手段侵扰他人;</span></p>
+      <p class="text-color"><span class="text-font">(2)违法收集、发布个人信息,发布与任何第三方有关或者违法向任何第三方收集包含个人信息的内容,包括用户通过在线课堂可能披露的个人信息,例如的电话号码、街道地址、姓名等;</span></p>
+      <p class="text-color"><span class="text-font">(3)未经本人的明确许可,公布其个人信息,包括但不限于手机号码、住址、姓名等;</span></p>
+      <p class="text-color"><span class="text-font">(4)公开发布私人对话,包括公开自其他用户收到的私人消息。</span></p>
+      <p class="text-color"><span class="text-font">(5)以其他任何方式非法获取、使用、出售、提供其他用户的个人信息;</span></p>
+      <p class="text-color"><span class="text-font">(6)其他违反法律法规或国家政策以及损害我们及他人合法权益的行为。</span></p>
+      <p class="text-color"><span class="text-font">10、利用本服务服务从事违法犯罪活动。包括但不限于:</span></p>
+      <p class="text-color"><span class="text-font">(1)设立用于实施诈骗,传授犯罪方法,制作或者销售违禁物品、管制物品等违法犯罪活动的网站、通讯群组;</span></p>
+      <p class="text-color"><span class="text-font">(2)利用本服务发布涉及实施诈骗,制作或者销售违禁物品、管制物品以及其他违法犯罪活动的信息;</span></p>
+      <p class="text-color"><span class="text-font">(3)利用门墩儿招聘账号或本服务从事欺诈、传销、刷流量、好评等任何违法犯罪活动;</span></p>
+      <p class="text-color"><span class="text-font">(4)利用本服务从事洗钱、窃取商业秘密、窃取个人信息等违法犯罪活动。</span></p>
+      <p class="text-color"><span class="text-font">(5)在不符合中国有关法规的情况下,从中国大陆向境外传输资料信息。</span></p>
+      <p class="text-color"><span class="text-font">11、冒充任何人或机构。包括但不限于:</span></p>
+      <p class="text-color"><span class="text-font">(1)冒充门墩儿招聘工作人员或以虚伪不实的方式谎称或使人误认为门墩儿招聘与任何人或任何机构有关;</span></p>
+      <p class="text-color"><span class="text-font">(2)伪造标题或以其他方式使人误认为该内容为门墩儿招聘发布、传送;</span></p>
+      <p class="text-color"><span class="text-font">(3)仿冒、混淆他人账号昵称、头像或发布内容,或冒充、利用他人名义;</span></p>
+      <p class="text-color"><span class="text-font">(4)盗用他人头像或资料,冒充、利用他人名义。</span></p>
+      <p class="text-color"><span class="text-font">12、未能按照本服务的流程、规则进行注册、认证或使用本服务的,违反本服务功能限制或运营策略,或采取任何措施规避本服务的流程、规则、限制或策略的。</span></p>
+      <p class="text-color"><span class="text-font">13、使用本服务在本服务上制作、上传、发布、传播任何违反法律法规及其他规范性文件、本规范及《门墩儿招聘用户服务协议》的文字、图片等内容的。</span></p>
+      <p class="text-color"><span class="text-font">14、不遵守本协议之约定或教唆他人从事本协议所禁止的行为。</span></p>
+      <p class="text-color"><span class="text-font">15、违反任何相关的法律、法规、规章、条例等其他具有法律效力的规范。</span></p>
+      <p class="text-color"><span class="text-font">16、其他任何导致或可能导致我们与第三方产生纠纷、争议或诉讼的行为。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">四、违约责任</span></h2>
+      <h2 class="subtitle text-size"><span class="text-font">为了维护平台的良好秩序及用户权益,如果您出现违反有关法律法规以及本协议的情况,包括但不限于发布违法信息或本协议及本协议的关联协议中明令禁止发布的内容,以及恶意损害本网站声誉、商誉或违反有关法律法规、本协议以及相关网站、App 、小程序使用规则的行为,我们有权自行判断并视情节严重程度,决定采取以下一种或多种处置措施:警示、拒绝发布、删除信息、屏蔽或断开连接、限制功能、限期改正、限制账号功能、按照平台要求出具不再发生违规行为的保证及其他证明文件并根据实际情况决定是否恢复服务、暂停更新直至关闭账号、中止/终止向您提供部分/全部服务、封禁/回收用户账号、禁止重新注册、不予退还已支付费用、要求您赔偿门墩儿招聘因此遭受的全部损失(包括但不限于财产损害赔偿、名誉损害赔偿、律师费、交通费等因维权而产生的合理费用,定义下同)等处理措施。</span></h2>
+      <h2 class="subtitle text-size"><span class="text-font">此外,您的行为如涉及违法犯罪活动的,门墩儿招聘有权单方终止提供全部或部分智联产品与/或服务,并移交司法机关进行处理。如因上述处理措施给用户带来任何损失或者因此无法使用门墩儿招聘账号和服务的,门墩儿招聘对此不承担任何责任。</span></h2>
+      <h2 class="subtitle text-size"><span class="text-font">由此导致的一切不利后果,由用户自行承担;导致第三方损害的,用户应当独立承担责任;因用户的上述行为给我们造成不利后果的,用户应负责消除影响,并且赔偿我们因此遭受的一切损失,包括但不限于财产损害赔偿、名誉损害赔偿、律师费、交通费等因维权而产生的合理费用。</span></h2>
+      <p class="text-color"><span class="text-font">用户上传的内容非用户原创的,用户保证对该内容已经取得了合法授权(包含转授权),有权以《门墩儿招聘用户服务协议》及本规范约定的方式授权给门墩儿招聘使用,若用户违反上述保证,导致的任何争议或纠纷由用户自行解决;
+        <strong>由此给门墩儿招聘造成损失的,用户应当对门墩儿招聘承担赔偿责任,包括但不限于财产损害赔偿、名誉权损害赔偿、律师费、交通费等因维权而产生的合理费用。</strong></span></p>
+      <p class="text-color"><span class="text-font">如用户因违反《门墩儿招聘用户服务协议》或本规范导致第三方损害的,用户应当独立承担责任,门墩儿招聘不因此承担任何责任;如有任何第三方向我们主张权益,您应当为我们积极向第三方抗辩,或者按照我们的要求,为我们向第三方抗辩提供必要的配合与协助(包括但不限于提供与相关权利证明文件、资料、信息等);
+        <strong>造成门墩儿招聘损失的,用户应当对门墩儿招聘承担赔偿责任,包括但不限于财产损害赔偿、名誉权损害赔偿、律师费、交通费等因维权而产生的合理费用。</strong></span></p>
+      <h2 class="subtitle text-size"><span class="text-font">五、附则</span></h2>
+      <p class="text-color"><span class="text-font">1.本行为规范的订立、执行和解释及争议的解决均应适用中国法律并受中国法院管辖。如您与门墩儿招聘就本规范内容或其执行发生任何争议,双方应友好协商解决;协商不成时,任何一方均可向苏州工业园区人民法院提起诉讼。</span></p>
+      <p class="text-color"><span class="text-font">2.本规范自发布之日起施行,门墩儿招聘有权基于法律规定的变化、平台功能的调整,以及自身管理经验的不断丰富等,出于维护门墩儿招聘平台秩序的目的,不断修订并完善本规范。用户应经常查阅并了解本规范,以便获得最新信息。</span></p>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+@import '../../static/style/protocol/index.scss';
+</style>

+ 27 - 0
pagesB/agreement/WorkplaceCommunityPolicy.vue

@@ -0,0 +1,27 @@
+<template>
+  <div class="conter">
+    <div class="Protocol">
+      <h1 class="segment">职场社区政策</h1>
+      <p class="text-color"><span class="text-font">我们将竭诚为会员提供专业的“服务”,为更好提供服务,我们制定此职场社区政策。职场社区政策旨在规范何种行为在社区是被允许的,何种行为在社区是被禁止的。您在使用本网站服务时,应该遵守此规定。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">1. 个人资料</span></h2>
+      <p class="text-color"><span class="text-font">本网站要求会员必须提供真实的姓名和准确的个人资料。您在使用本网站服务时不得以使用他人姓名、图像或其他个人信息来假冒他人,也不得以任何其他形式使他人误解您隶属于某个企业或组织。同时不得使用不属于您本人的账户,不得创建除本网站用户协议规定以外的非真实用户。在上传档案头像时,必须是您本人的图像。同时,您不得操控识别码以伪装通过服务传递任何消息或动态来源。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">2. 发布内容</span></h2>
+      <p class="text-color"><span class="text-font">您在本网站交流及所发布之内容务必与本网站宗旨相一致,不得做出不当行为。您在本网站使用服务时所创建之内容,必须遵守本网站相关政策规定,并且所创建之内容应该与本网站服务内容相关。不得邀请您所无法识别其真实身份的人加入您的人脉,不要将错误的或未经证实信息在本网站进行分享。你所发布的内容不得明示或暗示含有任何色情内容。您也不得发布任何含有暴力或妨害风化等内容,同时不得在使用本网站服务时从事任何违法犯罪活动。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">3. 社区氛围</span></h2>
+      <p class="text-color"><span class="text-font">本网站所倡导之交流应该是积极、向上、充满正能量氛围。不得在本网站发表攻击他人或企业的不当言论。此类言论包含但不限于辱骂或羞辱性言语、讽刺及影射性言论、未经允许发布他人未经公开的个人资料、或者煽动他人进行此类行为。</span></p>
+      <p class="text-color"><span class="text-font">您不得利用本网站之服务发表鼓吹暴力、组织犯罪、煽动仇恨、散布偏见言论等行为。您不得利用本网站之服务为恐怖组织进行招募,或者利用本网站为恐怖组织进行宣传或报道恐怖活动等。</span></p>
+      <p class="text-color"><span class="text-font">您不得在使用本网站服务时发布暴力或露骨内容,或意图恫吓或羞辱他人。不允许鼓吹、组织、描绘或助长犯罪活动;不允许叙述或鼓吹指导武器制作、药物滥用和盗窃威胁等相关内容;也不允许宣传或鼓励自杀或任何自我伤害的举动,包括自残、绝食或暴食等内容或活动。</span></p>
+      <p class="text-color"><span class="text-font">您不得在使用本网站服务时发送无特定对象、无相关性、不想要、未经请求、未经授权、不恰当的商业或促销,或无故重复的消息。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">4. 合法使用</span></h2>
+      <p class="text-color"><span class="text-font">您在使用本网站服务时所分享和使用的作品、商标、私人信息或商业秘密等必须首先保证您具有合法的权限。本网站所提供的服务不应被用于非法活动、宣传非法产品或侵犯他人权利。请勿使用本网站进行诈骗或企图蒙骗他人的活动。</span></p>
+      <p class="text-color"><span class="text-font">您在使用本网站的服务时您不得在未经授权的情况下向不认识的本网站会员征求邮箱地址或其他个人资料;不得使用、揭露或散布任何以违反本网站政策或协议之方式取得的资料;不得发布您未获同意即公开的信息。</span></p>
+      <p class="text-color"><span class="text-font">您在使用本网站服务时必须遵守相关法律、法规,包括但不限于知识产权法、税法及监管要求。您不得使用本网站服务从事传销活动、参与欺诈等违法犯罪活动。</span></p>
+      <p class="text-color"><span class="text-font">您在使用本网站服务时不得违反他人的知识产权,包括版权、专利、商标、商业机密或其他所有权。</span></p>
+      <p class="text-color"><span class="text-font">您在使用本网站服务时不得侵害本网站权利。您必须遵守我们的规则、协议和政策。</span></p>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+@import '../../static/style/protocol/index.scss';
+</style>

+ 44 - 0
pagesB/agreement/index.vue

@@ -0,0 +1,44 @@
+<template>
+  <view class="pb-120">
+    <view class="card">
+      <uni-list>
+        <uni-list-item
+          v-for="item in list"
+          :clickable="true"
+          :key="item.title"
+          :title="item.title"
+          showArrow
+          :rightText="item.rightTex || ''"
+          @click="handleToLink(item)"
+        >
+        </uni-list-item>
+      </uni-list>
+    </view>
+  </view>
+</template>
+
+<script setup>
+const list = [
+	{	title: '用户协议',	path: '/pagesB/agreement/user' },					
+	{	title: '隐私协议',	path: '/pagesB/agreement/privacy' },					
+	{	title: '版权政策',	path: '/pagesB/agreement/CopyrightPolicy' },					
+	{	title: '职场社区政策',	path: '/pagesB/agreement/WorkplaceCommunityPolicy' },					
+	{	title: '用户行为规范',	path: '/pagesB/agreement/UserBehaviorNorms' },					
+]
+
+const handleToLink = (item) => {
+	uni.navigateTo({
+		url: item.path
+	})
+}
+</script>
+<style scoped lang="scss">
+:deep(.uni-list-item) {
+	height: 120rpx !important;
+	line-height: 120rpx !important;
+}
+:deep(.uni-list-item__content-title) {
+	font-size: 32rpx !important;
+	font-weight: 500;
+}
+</style>

+ 51 - 0
pagesB/agreement/privacy.vue

@@ -0,0 +1,51 @@
+<template>
+  <view class="conter">
+    <view class="Protocol">
+      <h1 class="segment">隐私政策</h1>
+      <h2 class="subtitle text-size"><span class="text-font">引言</span></h2>
+      <p class="text-color"><span class="text-font">苏州识喜识谊信息科技有限公司(“识喜识谊”、“我们”、“我们的”)尊重您的隐私,致力于保护您在使用我们的网站www.menduner.com、企业公众服务号、小程序、其他线上产品和服务(“网站”)、在您注册网站会员时、或在您以其他方式与我们互动时,提供给我们、或我们收集的属于您的个人信息的隐私性、保密性和安全性。</span></p>
+      <p class="text-color"><span class="text-font">本网站关注每一位用户的权益,珍视您的隐私,此隐私保障政策解释了我们的个人信息实际操作和您能够选择的个人信息被使用的方式。此隐私保障政策是我们对您个人信息保护的承诺,并已被所有识喜识谊其关联公司或子公司所采纳。</span></p>
+      <p class="text-color"><span class="text-font">在下列情形下,您将被要求同意此隐私保障政策中的各项条款:注册会员、享受会员服务、注册加入活动或促销活动、通过网站与我们联系或适用法律法规所要求的其他情况。除此之外,您对本网站的继续使用将视为您已同意该隐私保障政策的各项条款。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">数据使用</span></h2>
+      <p class="text-color"><span class="text-font">我们给您提供的服务将基于您提供的数据。</span></p>
+      <p class="text-color"><span class="text-font">在您确定使用我们的服务,以及您使用服务的方式及设置后,我们将确定如何使用您的个人数据。为更好为您提供服务,我们在使用您的个人数据进行服务时,将借助智能系统进行处理。</span></p>
+      <p class="text-color"><span class="text-font">为更好提供服务,我们将对服务内容进行更新,由此造成的数据采集内容及使用方式的变更,我们将及时通知您,同时可能会对本政策进行修改。</span></p>
+      <p class="text-color"><span class="text-font">我们在使用您的个人信息时将以合理、合法为原则。我们将在以下几个方面使用您的个人信息。</span></p>
+      <p class="text-color"><span class="text-font">1. 我们收集的个人信息将被用于确保我们产品和服务的功能和安全、验证您的身份、防止并追究欺诈或其他不当使用的情形。</span></p>
+      <p class="text-color"><span class="text-font">2. 我们收集的个人信息将被用于我们的产品和服务开发,尽管一般情况下,我们为此目的仅使用综合信息和统计性信息。</span></p>
+      <p class="text-color"><span class="text-font">3. 我们收集的个人信息将被用于与您进行交流,例如在本网站产品或服务更新、发布的第一时间向您发出通知。</span></p>
+      <p class="text-color"><span class="text-font">4. 我们所收集的个人信息将被用于进行产品的个性化设计,并向您提供更优化的服务。</span></p>
+      <p class="text-color"><span class="text-font">5. 为了更好的为您提供服务,本网站可能将您同意公开的姓名、简历信息、个人标签、职位等信息推送给适合的招聘或求职用户。</span></p>
+      <p class="text-color"><span class="text-font">6. 如果您参与本网站举办的调查、抽奖、竞赛或类似推广活动,我们会将您提供的个人信息用于管理此类活动。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">信息分享</span></h2>
+      <p class="text-color"><span class="text-font">1. 与本公司其他产品或关联公司产品或共享:为更好的向您提供服务,推荐您可能感兴趣的信息,您的部分个人信息可能会共享给本公司其他产品或者关联公司的产品。我们只会共享必要的个人信息,例如共享您的行为偏好,以向您推荐同类信息。</span></p>
+      <p class="text-color"><span class="text-font">2. 为更好提供服务,我们可能会在《用户协议》框架内及法律允许的范围内将您的信息与第三方信息进行匹配,从而为您量身定制相关信息。在使用过程中,我们会采取一切必要措施保证您的个人数据安全。随着我们业务的持续发展,我们以及我们的关联公司有可能进行合并、收购、资产转让或类似的交易,我们收集的相关信息有可能作为此类交易的一部分而被转移。</span></p>
+      <p class="text-color"><span class="text-font">3. 法律声明</span></p>
+      <p class="text-color"><span class="text-font">如果我们或我们的关联公司依照法律、法规、法院命令、监管机构命令的要求,或根据政府的行为、监管要求或请求,或为了保护您、我们和他人的权利与安全,我们也许需要共享您的数据。</span></p>
+      <p class="text-color"><span class="text-font">我们将根据法律及相关政策要求,在以下情况下公开您的信息: (1) 相关政府执法部门在执法过程中需要进行协助的;(2) 根据《用户协议》可以公开的;(3) 针对任何第三方主张或指控进行调查与自我辩护;(4) 保护我们“服务”的安全性或完整性;或 (5) 行使或保护本网站、“会员”、职员或他人的权利和安全。在应法律及相关政策要求需要提供您个人数据时,我们将根据情况尽力通知您。</span></p>
+      <p class="text-color"><span class="text-font">4. 管理变更或出售</span></p>
+      <p class="text-color"><span class="text-font">将来我们可能会被出售、合并等管理主体变更时,您的数据依然会被新的管理主体共享,但本政策会被继续遵守。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">您的选择与义务</span></h2>
+      <p class="text-color"><span class="text-font">1. 数据保留</span></p>
+      <p class="text-color"><span class="text-font">在您使用我们账号的过程中,您的数据将被我们保留。此数据包含您提供的数据及我们经过推断而产生的数据等。</span></p>
+      <p class="text-color"><span class="text-font">2. 访问与控制个人数据的权利</span></p>
+      <p class="text-color"><span class="text-font">对于您的个人数据,您可以随时查看和更改,同时我们对于您数据的收集、使用、共享提供了多种选择。</span></p>
+      <p class="text-color"><span class="text-font">数据管理:</span></p>
+      <p class="text-color"><span class="text-font">删除数据:您可以要求我们清除或删除您的全部或部分个人数据。</span></p>
+      <p class="text-color"><span class="text-font">更改或纠正数据: 您可以通过您的帐号编辑您的部分个人数据。</span></p>
+      <p class="text-color"><span class="text-font">限制对数据的使用:您可以要求我们停止使用您的全部或部分个人数据。</span></p>
+      <p class="text-color"><span class="text-font">3. 注销帐号</span></p>
+      <p class="text-color"><span class="text-font">在您被注销帐号之后,我们仍然会保留您的部分数据。</span></p>
+      <p class="text-color"><span class="text-font">在您注销您的本网站帐号后24小时内,您的个人数据将不再向其他人显示。以下情形除外:</span></p>
+      <p class="text-color"><span class="text-font">法律或相关政策要求、遵守监管要求、解决争议、保证安全、防止欺诈与滥用、执行《用户协议》或满足您向我们“退订”某些消息的请求,即使您注销帐号,本网站仍可能保留您的部分个人数据。在您的帐号注销后,我们将保留不可识别个人的信息。</span></p>
+      <p class="text-color"><span class="text-font">您已与他人分享的信息在您注销帐号或从职业档案或收件箱中删除后,将仍对他人公开,并且我们无法控制其他“会员”从我们的“服务”中复制的信息。与注销帐号相关的群组内容、评分或点评内容,会以“未知用户”列为内容来源。您的职业档案可能继续显示在其他方的服务中,直到其更新缓存。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">联系信息</span></h2>
+      <p class="text-color"><span class="text-font">您可以联系我们或使用其他方式来解决任何投诉。</span></p>
+      <p class="text-color"><span class="text-font">若您对本《隐私政策》有任何疑问或投诉,您可以通过邮件来联系我们。</span></p>
+    </view>
+  </view>
+</template>
+
+<style scoped lang="scss">
+@import '../../static/style/protocol/index.scss';
+</style>

+ 78 - 0
pagesB/agreement/user.vue

@@ -0,0 +1,78 @@
+<template>
+  <view class="conter">
+    <view class="Protocol">
+      <h1 class="segment">用户协议</h1>
+      <h2 class="subtitle text-size"><span class="text-font">1.引言</span></h2>
+      <p class="text-color"><span class="text-font">1.1苏州识喜识谊信息科技有限公司同意按照本协议的规定及其不时发布的操作规则提供门墩儿网站,小程序(www.menduner.com)(以下称“本网站”或“我们”)网络服务,为获得网络服务,服务使用人(以下称“用户”或“您”,包括酒店管理者及相关从业人员、企业用户等)应当同意本协议的全部条款并按照页面上的提示完成全部的注册程序。用户在进行注册程序过程中勾选“我已阅读并同意”选项表示您完全接受并遵守本用户协议项下的全部条款,请您务必仔细阅读,充分理解协议的条款内容后再点击同意确认,您点击同意后即视为已接受本用户协议为构成对双方具有约束力的法律文件。</span></p>
+      <p class="text-color"><span class="text-font">1.2用户注册完成后,用户账号和密码由用户自行保管,用户应当对以其账号进行的所有活动和事件负法律责任。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">2.用户使用限制及收费</span></h2>
+      <p class="text-color"><span class="text-font">2.1本网站的会员只适用于已年满十八周岁,且具备相应的民事行为能力签订本用户协议。十六周岁至十八周岁的未成年人如希望使用服务,请由未成年人的监护人注册账号和填写相关信息。本网站不对任何不具备完全民事行为能力的主体开放,完全民事行为能力以外的人不是本网站的合格使用者,本网站有权采取包括但不限于注销账户的处理措施,并向使用者的监护人或负责人索偿。</span></p>
+      <p class="text-color"><span class="text-font">2.2本网站有权限制您使用“服务”时与会员建立联系和进行互动的方式。</span></p>
+      <p class="text-color"><span class="text-font">2.3本网站保留限制您使用“服务”的权利,包括限制您联系人的数量和您与其他会员互动的能力。如果本网站认为您可能违反了本合同或法律,或对“服务”使用不当 (如违反任何应做或禁止事项或职场社区政策),本网站将保留限制、暂停或终止您帐号的权利。因本网站主要接纳的会员为酒店管理者及相关从业人员、企业用户等,如果本网站认为您不属于上述会员范畴,本网站将保留限制、暂停或终止您帐号的权利。</span></p>
+      <p class="text-color"><span class="text-font">2.4本网站对部分“服务”收取一定的费用。在此情况下,本网站会在相关页面上做明确的提示。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">3.用户授权</span></h2>
+      <p class="text-color"><span class="text-font">用户应确保提供给本网站的所有内容、反馈和个人信息均具有真实性、准确性、完整性以及合法有效性,同时用户也授予门墩儿网站享有如下权限:在不需要另行通知并取得用户授权的情况下免费在全球范围内,可以使用、复制、修改、传播、发表、加工用户通过本网站“服务”提供的信息和内容,以上权限可以转让及可以授权给第三方。</span></p>
+      <p class="text-color"><span class="text-font">本网站在使用这些权限时有如下限制:</span></p>
+      <p class="text-color"><span class="text-font">3.1您可以通过删除“服务”里的特定内容或注销帐号,来终止您授予本网站的权限,但该内容已经被您或他人分享时已经被他人复制、重新分享或保存了该内容的除外;</span></p>
+      <p class="text-color"><span class="text-font">3.2您的内容将不会被我们用于商业目的。但本网站免费享有在您的内容旁发布广告的权利;</span></p>
+      <p class="text-color"><span class="text-font">3.3如果我们想给予第三方在“服务”范围之外发布您发表内容的权利,须取得您的同意。但是,如果您选择“公开,我们将允许会员将您的公开动态嵌入第三方服务,还将允许搜索引擎通过其服务将这些公开内容作为可被查找的内容。</span></p>
+      <p class="text-color"><span class="text-font">3.4 您的内容可能会被我们在不更改本身含义的情况下进行编辑或修改。</span></p>
+      <p class="text-color"><span class="text-font">3.5 我们将根据您所提供的信息及数据,向您推送相关的信息及服务。</span></p>
+      <p class="text-color"><span class="text-font">3.6在遵守《隐私政策》的条款以及您同意前提下,您和本网站同意,我们能访问、保存、处理和使用您提供的任何信息和个人数据。</span></p>
+      <p class="text-color"><span class="text-font">3.7您承诺您所提供的档案是真实的,同时您所发布的信息和内容没有任何侵权。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">4.用户使用规则</span></h2>
+      <p class="text-color"><span class="text-font">4.1用户义务</span></p>
+      <p class="text-color"><span class="text-font">4.1.1遵守中华人民共和国相关法律法规,包括但不限于《中华人民共和国数据安全法》《中华人民共和国网络安全法》《个人信息保护法》等有关法律和法规;</span></p>
+      <p class="text-color"><span class="text-font">4.1.2提供准确信息,并及时更新信息;在职业档案中使用真实姓名。</span></p>
+      <p class="text-color"><span class="text-font">4.2用户禁止事项:</span></p>
+      <p class="text-color"><span class="text-font">4.2.1在本网站上创建虚假身份,并以此为本人或他人谋取不正当利益;</span></p>
+      <p class="text-color"><span class="text-font">4.2.2未经本网站同意进行“服务”数据采集或复制“服务”中的职业档案等其他信息;</span></p>
+      <p class="text-color"><span class="text-font">4.2.3未经本网站同意,复制、使用、公开或传播任何通过“服务”获得的信息,无论是直接获得还是通过第三方(例如搜索引擎)获得的;</span></p>
+      <p class="text-color"><span class="text-font">4.2.4侵犯其他任何第三方专利权、著作权、商标权、名誉权或其他任何合法权益;</span></p>
+      <p class="text-color"><span class="text-font">4.2.5侵犯本网站的知识产权或其他权利;</span></p>
+      <p class="text-color"><span class="text-font">4.2.6未经本网站明确同意,不得以本网站名义从事任何商业及收费商业活动;</span></p>
+      <p class="text-color"><span class="text-font">4.2.7未经本网站同意,以宣传您的本网站档案或某个本网站群组以外的任何目的,深层链接到“服务”;</span></p>
+      <p class="text-color"><span class="text-font">4.2.8 使用自动程序(即“bots”)或其他自动方法访问“服务”、添加或下载通讯录、发送或重定向信息;</span></p>
+      <p class="text-color"><span class="text-font">4.2.9以任何竞争目的监控“服务”的可用性、性能或功能;</span></p>
+      <p class="text-color"><span class="text-font">4.2.10 参与“建立构架”、“制作镜像”或其他模拟“服务”外观或功能的活动;</span></p>
+      <p class="text-color"><span class="text-font">4.2.11擅自修改本网站“服务”或其外观;</span></p>
+      <p class="text-color"><span class="text-font">4.2.12干扰“服务”的运营、或给“服务”带来不合理的负荷(例如群发邮件、拒绝服务攻击、病毒、游戏运算);和/或违反在您注册或开始使用某项特定“服务”时所规定的《职业社区政策》或任何附加条款。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">5.服务变更</span></h2>
+      <p class="text-color"><span class="text-font">本网站可能会修改、暂停或取消某些本网站所提供的服务,或会自行更改价格,本网站无法承诺保存或持续展示您发布的任何信息和内容。在法律允许的范围内,这些更改可能在向您发送通知时开始生效。本网站没有义务保存、维护您或他人提供的任何信息和内容,也没有义务提供这些内容和信息的副本。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">6. 知识产权</span></h2>
+      <p class="text-color"><span class="text-font">6.1本网站保留“服务”中的所有知识产权。使用“服务”并不代表您对我们的“服务”或通过“服务”提供的内容或信息拥有任何所有权。本网站在本服务中提供的内容(包括但不限于网页、文字、图片、音频、视频、图表等)的知识产权均归本网站所有,但本服务中涉及广告的知识产权由相应广告商享有的,用户在使用本服务前对自己发布的内容已合法取得知识产权的、在内容中声明了转载或经过第三方授权,且非通过载入第三方网页的除外。</span></p>
+      <p class="text-color"><span class="text-font">6.2除另有特别声明外,本网站提供服务时所依托软件的著作权、专利权及其他知识产权均归门墩儿网站所有。</span></p>
+      <p class="text-color"><span class="text-font">6.3与“服务”相关的商标和标志是其各自所有者的商标。本网站以及本网站服务使用的其他本网站商标、服务标志、图片和标志均为本网站的商标或注册商标。未经本网站事先书面同意,用户不得将本网站的标识以任何方式展示或使用或用作其他处理,也不得向他人表明用户有权展示、使用、或其他有权处理本网站标识的行为。</span></p>
+      <p class="text-color"><span class="text-font">6.4 基于对数据的合法加工而获得的具有竞争性的数据权益,除法律法规另有规定外,我们享有独立的使用权益而无须获得您的同意。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">7.风险提示</span></h2>
+      <p class="text-color"><span class="text-font">7.1由于本网站一般不对会员及他人提供的内容进行审查,如果您在使用服务时看到不准确、不完整、过时、有误导性、非法、冒犯性或有害的内容或信息,本网站不对以上信息负责。</span></p>
+      <p class="text-color"><span class="text-font">7.2如果您通过本网站“服务”上的链接访问或使用第三方 APP 、小程序或网站进行访问,通过以上方式进行访问时,他们可能会获取并使用您的相关信息,本网站对上述风险不承担责任。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">8.免责声明和有限责任</span></h2>
+      <p class="text-color"><span class="text-font">8.1本网站对由于政府禁令、现行生效的适用法律或法规的变更、火灾、地震、动乱、战争、停电、通讯线路中断、黑客攻击、计算机病毒侵入或发作、电信部门技术调整、因政府管制而造成网站的暂时性关闭等任何影响网络正常运营的不可预见、不可避免、不可克服和不可控制的事件(“不可抗力事件”),以及他人蓄意破坏、本网站工作人员的疏忽或不当使用,正常的系统维护、系统升级,或者因网络拥塞而导致本网站不能访问而造成的本网站所提供的信息及数据的延误、停滞或错误,以及使用者由此受到的一切损失不承担任何责任;</span></p>
+      <p class="text-color"><span class="text-font">8.2本网站不对用户的线下行为负责。企业用户及个人用户均应审慎对待他方之行为,因为他方之行为给用户造成任何不利影响的,本网站不承担任何法律责任。</span></p>
+      <p class="text-color"><span class="text-font">8.3本网站对于向用户免费提供的服务或向用户赠送的任何产品或者服务的质量缺陷及其引发的任何损失,本网站无需承担任何责任。</span></p>
+      <p class="text-color"><span class="text-font">8.4对于本网站为使用者提供便利而设置的外部链接网址,本网站并不保证其准确性、安全性和完整性,亦并不代表本网站对其链接内容的认可,请使用者谨慎确认后使用,本网站对由此导致的任何损失或伤害不承担任何责任。</span></p>
+      <p class="text-color"><span class="text-font">8.5用户因使用本网站产生损失而提出的索赔金额,以用户向本网站已交付的款项之和为限。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">9. 协议终止</span></h2>
+      <p class="text-color"><span class="text-font">9.1双方可于任何时候终止本协议。</span></p>
+      <p class="text-color"><span class="text-font">9.2本网站或您均可在任何时间通知对方终止本协议。一旦协议终止,您就失去了访问或使用“服务”的权利。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">10. 适用法律和纠纷解决</span></h2>
+      <p class="text-color"><span class="text-font">若您我双方发生法律纠纷,双方同意在苏州工业园区人民法院解决,适用中华人民共和国的法律。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">11.适用语言及其他</span></h2>
+      <p class="text-color"><span class="text-font">11.1在法律允许的范围内,本合同的中文版本将具法律效力,其他语言版本仅起参考作用。</span></p>
+      <p class="text-color"><span class="text-font">11.2您不得在没有取得本网站同意的情况下将本合同(包括您的会员身份或“服务”使用权)转让或转移给任何人。但是,您同意本网站可以在没有您同意的情况下将本合同转让给关联机构或本网站的收购方。本合同不存在第三方受益者。</span></p>
+      <p class="text-color"><span class="text-font">11.3用户理解并同意,本网站对于用户所有的通知均可以通过网页公告、电子邮件、手机短信、微信模板信息方式进行;该等通知于发送之日视为已送达收件人。</span></p>
+      <p class="text-color"><span class="text-font">11.4用户对于本网站的通知应当通过本网站对外正式公布的通信地址、电子邮件地址等联系信息进行送达。</span></p>
+      <h2 class="subtitle text-size"><span class="text-font">12. 联系信息</span></h2>
+      <p class="text-color"><span class="text-font">如果您想向我们发送通知或送达法律文件,请联系我们:</span></p>
+      <p class="text-color"><span class="text-font">地址:江苏省苏州工业园区林泉街 399 号东南大学国家 大学科技园(苏州)南工院(2#)304 室</span></p>
+      <p class="text-color"><span class="text-font">邮编:215123</span></p>
+      <p class="text-color"><span class="text-font">公司:苏州识喜识谊信息科技有限公司</span></p>
+      <p class="text-color"><span class="text-font">邮箱:services@menduner.com</span></p>
+    </view>
+  </view>
+</template>
+
+<style scoped lang="scss">
+@import '../../static/style/protocol/index.scss';
+</style>

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä