Browse Source

feat 推荐官

yezhihao 3 months ago
parent
commit
911b35e254

+ 17 - 0
App.vue

@@ -1,4 +1,6 @@
 <script>
+import { openPushAd } from '@/common/pushAdStore.js'
+
 export default {
   onLaunch: function () {
 	uni.removeStorageSync('web_config')
@@ -605,6 +607,7 @@ export default {
     }
 
     //#endif
+	
   },
   onHide: function () {
     console.log("App Hide");
@@ -626,6 +629,20 @@ export default {
 				// 监听:点击通知(用户点击通知栏的通知)
 				jpushModule.addNotificationListener((res) => {
 					console.log('点击了通知:', res)
+					// 统一活动广告:extras 带 adImage / imageUrl / img 时弹出(与 page 可同时存在)
+					if (res.notificationEventType == 'notificationOpened' && res.extras && res.extras?.notice_type == 'system_notice') {
+            setTimeout(() => {
+              // 跳转我的页面后再打开公告
+							uni.switchTab({
+                url: '/pages/index/index',
+                success: () => {
+                  openPushAd({
+                    adImage: res.extras?.notice_image_url
+                  })
+                }
+              })
+						}, 500)
+					}
 					// 可在这里跳转到指定页面(比如根据通知的extra参数跳转)
 					if (res.notificationEventType == 'notificationOpened' && res.extras && res.extras.page) {
 						// tab页面

+ 29 - 0
common/pushAdStore.js

@@ -0,0 +1,29 @@
+import Vue from 'vue'
+
+/**
+ * 全局推送/活动广告弹窗状态(App.vue 内统一挂载一层 UI)
+ * 极光 extras 可约定字段:adImage(必填,图片 URL)、adUrl(可选,点击跳转)
+ */
+export const pushAdState = Vue.observable({
+	visible: false,
+	imageUrl: '',
+	linkUrl: '',
+	extras: null
+})
+
+export function openPushAd(payload) {
+	if (!payload || typeof payload !== 'object') return
+	const imageUrl = payload.adImage || payload.imageUrl || payload.img
+	if (!imageUrl) return
+	pushAdState.imageUrl = imageUrl
+	pushAdState.linkUrl = payload.adUrl || payload.linkUrl || payload.url || ''
+	pushAdState.extras = payload
+	pushAdState.visible = true
+}
+
+export function closePushAd() {
+	pushAdState.visible = false
+	pushAdState.imageUrl = ''
+	pushAdState.linkUrl = ''
+	pushAdState.extras = null
+}

+ 3 - 3
common/queue.js

@@ -37,9 +37,6 @@ module.exports = {
 	// 登录判断
 	toLogin(type) {
 		let url = '/pages/public/loginV2'
-		// #ifdef H5
-		url = '/pages/public/login'
-		// #endif
 		switch (type) {
 			case 'navigateTo':
 				uni.navigateTo({
@@ -94,6 +91,9 @@ module.exports = {
 			title: title
 		});
 	},
+	hideLoading() {
+		uni.hideLoading();
+	},
 	showToast(title) {
 		uni.showToast({
 			title: title,

+ 105 - 0
components/global-push-ad/global-push-ad.vue

@@ -0,0 +1,105 @@
+<template>
+	<view v-if="visible" class="gpa-mask" @click="onMask">
+		<view class="gpa-box" @click.stop>
+			<image
+				class="gpa-img"
+				:src="imageUrl"
+				mode="widthFix"
+				@click="onImageClick"
+			/>
+			<view class="gpa-close" @click="close">×</view>
+		</view>
+	</view>
+</template>
+
+<script>
+import { pushAdState, closePushAd } from '@/common/pushAdStore.js'
+
+export default {
+	name: 'GlobalPushAd',
+	computed: {
+		visible() {
+			return pushAdState.visible
+		},
+		imageUrl() {
+			return pushAdState.imageUrl
+		},
+		linkUrl() {
+			return pushAdState.linkUrl
+		}
+	},
+	methods: {
+		close() {
+			closePushAd()
+		},
+		onMask() {
+			this.close()
+		},
+		onImageClick() {
+			const url = this.linkUrl
+			if (!url) return
+			// #ifdef APP-PLUS
+			if (/^https?:\/\//i.test(url)) {
+				plus.runtime.openURL(url)
+				return
+			}
+			// #endif
+			// #ifdef H5
+			if (/^https?:\/\//i.test(url)) {
+				window.location.href = url
+				return
+			}
+			// #endif
+			if (url.startsWith('/')) {
+				uni.navigateTo({ url, fail: () => uni.switchTab({ url }) })
+			}
+		}
+	}
+}
+</script>
+
+<style scoped>
+.gpa-mask {
+	position: fixed;
+	left: 0;
+	right: 0;
+	top: 0;
+	bottom: 0;
+	z-index: 99999;
+	background: rgba(0, 0, 0, 0.55);
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	padding: 48rpx;
+	box-sizing: border-box;
+}
+
+.gpa-box {
+	position: relative;
+	max-width: 100%;
+	max-height: 85vh;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+}
+
+.gpa-img {
+	width: 600rpx;
+	max-width: 100%;
+	display: block;
+	border-radius: 16rpx;
+}
+
+.gpa-close {
+	margin-top: 32rpx;
+	width: 72rpx;
+	height: 72rpx;
+	line-height: 68rpx;
+	text-align: center;
+	font-size: 48rpx;
+	color: #fff;
+	background: rgba(0, 0, 0, 0.45);
+	border-radius: 50%;
+	flex-shrink: 0;
+}
+</style>

+ 459 - 0
components/poster/poster.vue

@@ -0,0 +1,459 @@
+<template>
+	<view class="popup-container" v-if="value" @click="close">
+		<view class="popup-wrapper">
+			<view class="popup-title" v-if="showTitle">您已经成为亿职赞第{{ rank }}位金牌推荐官</view>
+			<view class="poster-wrapper">
+				<image :src="posterImageSrc" class="poster-img" mode="aspectFit"></image>
+			</view>
+			<view class="poster-tip" v-if="showTip">推荐者的个人信息不会公开展示,请放心推荐</view>
+			<view class="button" @click.stop="handleSave">{{ saveBtnText }}</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			value: {
+				type: Boolean,
+				default: false
+			},
+			rank: {
+				type: Number,
+				default: 0
+			},
+			posterBase64Url: {
+				type: String,
+				default: ''
+			},
+			/** 纯 base64 时的图片类型,默认 png;若后端是 jpeg 可传 image/jpeg */
+			posterMime: {
+				type: String,
+				default: 'image/png'
+			},
+			showTitle: {
+				type: Boolean,
+				default: true
+			},
+			showTip: {
+				type: Boolean,
+				default: true
+			},
+			saveBtnText: {
+				type: String,
+				default: '保存海报并成为推荐官'
+			},
+		},
+		computed: {
+			posterImageSrc() {
+				const raw = this.posterBase64Url
+				if (!raw || typeof raw !== 'string') return ''
+				const t = raw.trim()
+				if (
+					t.startsWith('data:image') ||
+					t.startsWith('http://') ||
+					t.startsWith('https://') ||
+					t.startsWith('/') ||
+					t.startsWith('@/')
+				) {
+					return t
+				}
+				// 去掉换行空格,拼成 data URL 供 image 显示
+				const b64 = t.replace(/\s/g, '')
+				return `data:${this.posterMime};base64,${b64}`
+			}
+		},
+		data() {
+			return {
+				num: 990
+			}
+		},
+		methods: {
+			close() {
+				this.$emit('close')
+			},
+			
+			handleSave() {
+				const src = this.posterImageSrc
+				if (!src) {
+					uni.showToast({ title: '暂无海报', icon: 'none' })
+					return
+				}
+				// #ifdef H5
+				this._savePosterH5(src)
+				// #endif
+				// #ifdef APP-PLUS
+				this._savePosterApp(src)
+				// #endif
+			},
+
+			// #ifdef H5
+			_savePosterH5(src) {
+				if (src.startsWith('data:image')) {
+					try {
+						const parts = src.split(',')
+						const mimeMatch = parts[0].match(/:(.*?);/)
+						const mime = mimeMatch ? mimeMatch[1] : 'image/png'
+						const bstr = atob(parts[1])
+						const n = bstr.length
+						const u8arr = new Uint8Array(n)
+						for (let i = 0; i < n; i++) u8arr[i] = bstr.charCodeAt(i)
+						const blob = new Blob([u8arr], { type: mime })
+						const url = URL.createObjectURL(blob)
+						const a = document.createElement('a')
+						a.href = url
+						a.download = 'poster.' + (mime.indexOf('jpeg') !== -1 ? 'jpg' : 'png')
+						document.body.appendChild(a)
+						a.click()
+						document.body.removeChild(a)
+						URL.revokeObjectURL(url)
+						uni.showToast({ title: '已开始下载' })
+						this.$emit('saved')
+					} catch (e) {
+						uni.showToast({ title: '保存失败', icon: 'none' })
+					}
+					return
+				}
+				if (src.startsWith('http://') || src.startsWith('https://')) {
+					uni.showLoading({ title: '保存中...', mask: true })
+					fetch(src, { mode: 'cors' })
+						.then((res) => {
+							if (!res.ok) throw new Error('fetch fail')
+							return res.blob()
+						})
+						.then((blob) => {
+							uni.hideLoading()
+							const url = URL.createObjectURL(blob)
+							const a = document.createElement('a')
+							a.href = url
+							a.download = 'poster.png'
+							document.body.appendChild(a)
+							a.click()
+							document.body.removeChild(a)
+							URL.revokeObjectURL(url)
+							uni.showToast({ title: '已开始下载' })
+							this.$emit('saved')
+						})
+						.catch(() => {
+							uni.hideLoading()
+							uni.showToast({
+								title: '无法下载,请长按图片保存',
+								icon: 'none'
+							})
+						})
+					return
+				}
+				uni.showToast({ title: '请使用网络图或 base64 图片', icon: 'none' })
+			},
+			// #endif
+
+			// #ifdef APP-PLUS
+			/**
+			 * App 保存难在:① 相册权限各版本 Android 规则不同,误检会拦死;② saveImageToPhotosAlbum 多数机型要传 _doc/相对路径而非乱转 file://。
+			 * 这里按 DCloud 文档最小路径:Bitmap → _doc/xxx.png → uni.saveImageToPhotosAlbum(先试相对路径)。
+			 */
+			async _savePosterApp(src) {
+				const allowed = await this._ensureAlbumPermissionForSave()
+				if (!allowed) return
+
+				uni.showLoading({ title: '保存中...', mask: true })
+				const hide = () => {
+					try {
+						uni.hideLoading()
+					} catch (e) {}
+				}
+
+				try {
+					await this._withTimeout(this._savePosterAppCore(src), 50000, '保存超时,请重试')
+					hide()
+					uni.showToast({ title: '已保存到相册', icon: 'success' })
+					this.$emit('saved')
+				} catch (e) {
+					hide()
+					const msg = String((e && (e.errMsg || e.message)) || '保存失败')
+					if (/auth|permission|denied|拒绝|authorize/i.test(msg)) {
+						this._openAlbumSettingsModal()
+					} else {
+						uni.showToast({ title: msg.length > 36 ? '保存失败' : msg, icon: 'none' })
+					}
+				}
+			},
+
+			_withTimeout(p, ms, errMsg) {
+				return Promise.race([
+					Promise.resolve(p),
+					new Promise((_, reject) =>
+						setTimeout(() => reject(new Error(errMsg || '操作超时')), ms)
+					)
+				])
+			},
+
+			async _savePosterAppCore(src) {
+				let local = ''
+				if (src.startsWith('data:image')) {
+					local = await this._bitmapDataUrlToDocRel(src)
+				} else if (src.startsWith('http://') || src.startsWith('https://')) {
+					local = await new Promise((resolve, reject) => {
+						uni.downloadFile({
+							url: src,
+							success: (r) => {
+								if (r.statusCode === 200 && r.tempFilePath) resolve(r.tempFilePath)
+								else reject(new Error('图片下载失败'))
+							},
+							fail: () => reject(new Error('图片下载失败'))
+						})
+					})
+				} else {
+					local = src
+				}
+				await this._saveImageToAlbumTryPaths(this._albumPathCandidates(local))
+			},
+
+			/** 官方示例:loadBase64Data → save 到 _doc,返回相对路径 */
+			_bitmapDataUrlToDocRel(dataUrl) {
+				return new Promise((resolve, reject) => {
+					const name = 'poster_' + Date.now() + '.png'
+					const rel = '_doc/' + name
+					const id = 'posterBitmap' + Date.now()
+					const bm = new plus.nativeObj.Bitmap(id)
+					bm.loadBase64Data(
+						dataUrl,
+						() => {
+							bm.save(
+								rel,
+								{ overwrite: true },
+								() => {
+									try {
+										bm.clear()
+									} catch (e) {}
+									resolve(rel)
+								},
+								(err) => {
+									try {
+										bm.clear()
+									} catch (e) {}
+									reject(err || new Error('生成临时图失败'))
+								}
+							)
+						},
+						(err) => {
+							try {
+								bm.clear()
+							} catch (e) {}
+							reject(err || new Error('图片无法解析'))
+						}
+					)
+				})
+			},
+
+			_albumPathCandidates(p) {
+				const list = []
+				const add = (x) => {
+					if (x && list.indexOf(x) === -1) list.push(x)
+				}
+				add(p)
+				if (p && p.startsWith('file://')) {
+					add(p.replace(/^file:\/\//, ''))
+				}
+				if (typeof plus !== 'undefined' && plus.io && p) {
+					try {
+						add(plus.io.convertLocalFileSystemURL(p))
+					} catch (e) {}
+				}
+				return list
+			},
+
+			_saveImageToAlbumTryPaths(paths) {
+				if (!paths.length) {
+					return Promise.reject(new Error('没有可保存的路径'))
+				}
+				return paths.reduce(
+					(acc, fp) =>
+						acc.catch(() =>
+							new Promise((resolve, reject) => {
+								uni.saveImageToPhotosAlbum({
+									filePath: fp,
+									success: () => resolve(),
+									fail: (err) => reject(err || new Error('保存失败'))
+								})
+							})
+						),
+					Promise.reject(new Error('_'))
+				)
+			},
+
+			_openAlbumSettingsModal() {
+				uni.showModal({
+					title: '需要相册权限',
+					content: '保存图片到相册需要相册/存储权限,请在系统设置中开启后重试。',
+					confirmText: '去设置',
+					cancelText: '取消',
+					success: (r) => {
+						if (r.confirm && typeof uni.openAppAuthorizeSetting === 'function') {
+							uni.openAppAuthorizeSetting({})
+						}
+					}
+				})
+			},
+
+			/** 是否具备保存到相册所需权限;否:已弹窗引导,返回 false */
+			_ensureAlbumPermissionForSave() {
+				return new Promise((resolve) => {
+					if (typeof plus === 'undefined') {
+						resolve(true)
+						return
+					}
+					const os = plus.os.name
+					if (os === 'iOS') {
+						resolve(this._ensureIosAlbumPermissionForSave())
+						return
+					}
+					if (os === 'Android') {
+						this._ensureAndroidAlbumPermissionForSave().then(resolve)
+						return
+					}
+					resolve(true)
+				})
+			},
+
+			_ensureIosAlbumPermissionForSave() {
+				try {
+					if (typeof uni.getAppAuthorizeSetting !== 'function') {
+						return true
+					}
+					const s = uni.getAppAuthorizeSetting() || {}
+					const a = s.albumAuthorized
+					if (a === 'authorized' || a === 'limited') {
+						return true
+					}
+					if (a === 'denied') {
+						this._openAlbumSettingsModal()
+						return false
+					}
+					// not determined / 未返回:首次保存时由系统弹窗授权
+					return true
+				} catch (e) {
+					return true
+				}
+			},
+
+			_androidCheckPermissions(permissions) {
+				try {
+					const main = plus.android.runtimeMainActivity()
+					const PM = plus.android.importClass('android.content.pm.PackageManager')
+					const granted = PM.PERMISSION_GRANTED
+					for (let i = 0; i < permissions.length; i++) {
+						if (main.checkSelfPermission(permissions[i]) !== granted) {
+							return false
+						}
+					}
+					return true
+				} catch (e) {
+					return false
+				}
+			},
+
+			/**
+			 * Android 10+ 分区存储下,写入相册多走系统 MediaStore,预检 READ_MEDIA_IMAGES 常误判且与「保存」无关,会导致永远进不了保存。
+			 * 仅对 Android 9 及以下尝试申请 WRITE_EXTERNAL_STORAGE;其余交给 saveImageToPhotosAlbum 与系统弹窗。
+			 */
+			_ensureAndroidAlbumPermissionForSave() {
+				return new Promise((resolve) => {
+					if (!plus.android) {
+						resolve(true)
+						return
+					}
+					try {
+						const Build = plus.android.importClass('android.os.Build')
+						if (Build.VERSION.SDK_INT >= 29) {
+							resolve(true)
+							return
+						}
+						if (Build.VERSION.SDK_INT < 23) {
+							resolve(true)
+							return
+						}
+					} catch (e) {
+						resolve(true)
+						return
+					}
+					if (typeof plus.android.requestPermissions !== 'function') {
+						resolve(true)
+						return
+					}
+					const perms = ['android.permission.WRITE_EXTERNAL_STORAGE']
+					if (this._androidCheckPermissions(perms)) {
+						resolve(true)
+						return
+					}
+					plus.android.requestPermissions(
+						perms,
+						() => {
+							if (this._androidCheckPermissions(perms)) {
+								resolve(true)
+							} else {
+								this._openAlbumSettingsModal()
+								resolve(false)
+							}
+						},
+						() => {
+							this._openAlbumSettingsModal()
+							resolve(false)
+						}
+					)
+				})
+			},
+			// #endif
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.popup-container {
+		position: fixed;
+		left: 0;
+		top: 0;
+		width: 100vw;
+		height: 100vh;
+		background: rgba(0, 0, 0, 0.6);
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		.popup-wrapper {
+			.popup-title {
+				color: rgba(255, 255, 255, 1);
+				font-style: Bold;
+				font-size: 40rpx;
+				font-weight: 700;
+				line-height: 1.6;
+			}
+			.poster-wrapper {
+				width: 488rpx;
+				height: 1028rpx;
+				border-radius: 20rpx;
+				margin: 64rpx auto 32rpx;
+				.poster-img {
+					width: 100%;
+					height: 100%;
+				}
+			}
+			.poster-tip {
+				color: rgba(255, 255, 255, 1);
+				font-size: 24rpx;
+				font-weight: 400;
+				line-height: 40rpx;
+				text-align: center;
+			}
+			.button {
+				border-radius: 126px;
+				background: rgba(255, 255, 255, 1);
+				margin: 24rpx 30rpx;
+				text-align: center;
+				line-height: 90rpx;
+				color: rgba(0, 0, 0, 1);
+				font-size: 32rpx;
+				font-weight: 400;
+			}
+		}
+	}
+</style>

+ 2 - 1
constants/common.js

@@ -44,7 +44,8 @@ export const companyPeopleNum = [
 // 公司休息模式
 export const companyRestTime = [
 	'单休',
-	'双休'
+	'双休',
+	'大小周'
 ]
 
 // 公司加班情况

+ 2 - 0
main.js

@@ -8,6 +8,8 @@ import homeList from './components/home-list/homeList.vue'
 Vue.component('homeList', homeList);
 import homeuserList from './components/home-list/homeuserList.vue'
 Vue.component('homeuserList', homeuserList);
+import GlobalPushAd from '@/components/global-push-ad/global-push-ad.vue'
+Vue.component('GlobalPushAd', GlobalPushAd)
 Vue.config.productionTip = false
 Vue.prototype.$Request = HttpRequest;
 Vue.prototype.$queue = queue;

+ 3 - 5
manifest.json

@@ -2,8 +2,8 @@
     "name" : "亿职赞",
     "appid" : "__UNI__0B0096D",
     "description" : "",
-    "versionName" : "1.3.0",
-    "versionCode" : 130,
+    "versionName" : "1.3.2",
+    "versionCode" : 132,
     "transformPx" : false,
     "sassImplementationName" : "node-sass",
     /* 5+App特有相关 */
@@ -116,9 +116,7 @@
                         "name" : "amap6EYNrBli"
                     }
                 },
-                "push" : {
-                    "unipush" : {}
-                },
+                "push" : {},
                 "geolocation" : {
                     "amap" : {
                         "name" : "amap6EYNrBli",

+ 5 - 1
package/jobIntention/basicInfo.vue

@@ -102,7 +102,8 @@
 
 		<!-- 保存按钮 -->
 		<view class="button-section">
-			<view class="link-btn" v-if="!hasUserType" @click="handleIgnore">稍后填写</view>
+			<!-- 非邀请状态,且没有userType时显示 -->
+			<view class="link-btn" v-if="!hasUserType && noInvite" @click="handleIgnore">稍后填写</view>
 			<view class="btn" @click="messagebtn">保存</view>
 		</view>
 
@@ -226,6 +227,9 @@
 			// 判断是否有用户类型userType
 			hasUserType() {
 				return this.$queue.getData('userType')
+			},
+			noInvite() {
+				return this.scene != 1
 			}
 		},
 		watch: {

+ 14 - 0
pages.json

@@ -597,6 +597,20 @@
 				"navigationBarTitleText": "搜索公司",
 				"navigationStyle": "custom"
 			}
+		},
+		{
+			"path": "pages/my/recommendationOfficer/index",
+			"style": {
+				"navigationBarTitleText": "推荐官",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/my/recommendationOfficer/list",
+			"style": {
+				"navigationBarTitleText": "推荐官",
+				"navigationStyle": "custom"
+			}
 		}
 	],
 	"subPackages": [

+ 1 - 0
pages/index/game/gameList.vue

@@ -1,5 +1,6 @@
 <template>
   <view class="page-container">
+    <global-push-ad />
     <view class="page-bg">
       <view class="page-bottom"></view>
     </view>

+ 1 - 0
pages/index/index.vue

@@ -1,5 +1,6 @@
 <template>
 	<view style="background-color: #f2f2f7;">
+		<global-push-ad />
 		<!-- <template v-if="XCXIsSelect != '是'">
 			<view class="banner flex justify-center">
 				<view class="banner-box">

+ 1 - 0
pages/msg/index.vue

@@ -1,5 +1,6 @@
 <template>
 	<view>
+		<global-push-ad />
 		<view class="msg-box" :style="{ paddingTop: BarHeight + 'px' }">
 		<!-- <view class="msg-box"> -->
 			<!-- 顶部导航栏 -->

+ 62 - 11
pages/my/attachment.vue

@@ -11,10 +11,8 @@
 			</view>
 		</view>
 
-		<!-- <nav-bar title="基本信息"></nav-bar> -->
 		<view class="contain flex flex-direction">
 			<scroll-view scroll-with-animation scroll-y>
-				<!-- 我的公司 -->
 				<view class="usermain-item item-padding" v-for="item in content">
 					<view class="usermain-item-title c-flex-center">
 						<image
@@ -29,17 +27,31 @@
 						<u-icon name="more-dot-fill"></u-icon>
 					</view>
 				</view>
-				<empty :isShow="false" v-if="content.length == 0" />
+				<empty v-if="content.length == 0" />
 			</scroll-view>
 		</view>
-		<view class="buttons flex align-center">
-			<view class="button pain" @click="showTipPopup = true">Ai一键解析简历</view>
-			<view class="button primary" @click="goResumeUpload">简历上传</view>
-			<!-- <view class="button primary">
-				简历上传
-				<fileSelector title="" allowType=".doc,.docx,.xls,.xlsx,.pdf" @fileSelected="uploadResumes"
-					@filesChanged="onFilesChanged" />
-			</view> -->
+
+		<view>
+			<view class="ai-upload-tip" v-if="content.length != 0">
+				<image src="@/static/images/my/ai-logo.png" class="ai-logo"></image>
+				<view class="ai-tip-title">Ai简历一键解析,无需手动填写简历</view>
+				<view class="step-wrapper">
+					<view class="step-item">Step 1 点击简历上传按钮,上传PDF附件简历</view>
+					<view class="step-item">Step 2 找到附件简历,点击文件右侧“···”操作键</view>
+					<view class="step-img"></view>
+					<image src="@/static/images/my/resume-file.png" class="step-file-img"></image>
+					<view class="step-item">Step 3 点击"Ai解析”对简历进行一键同步</view>
+				</view>
+			</view>
+			<view class="buttons flex align-center">
+				<!-- <view class="button pain" @click="showTipPopup = true">Ai一键解析简历</view> -->
+				<view class="button primary" @click="goResumeUpload">简历上传</view>
+				<!-- <view class="button primary">
+					简历上传
+					<fileSelector title="" allowType=".doc,.docx,.xls,.xlsx,.pdf" @fileSelected="uploadResumes"
+						@filesChanged="onFilesChanged" />
+				</view> -->
+			</view>			
 		</view>
 		<!-- 简历解析确认弹窗 -->
 		<view class="popup-container" v-if="showTipPopup" @click="showTipPopup = false">
@@ -1007,6 +1019,45 @@ button {
 	}
 }
 
+// ai解析提示弹窗
+.ai-upload-tip {
+	border-radius: 20rpx;
+	background: rgba(255, 255, 255, 1);
+	margin: 50rpx 50rpx 40rpx;
+	box-shadow: 0 0 30rpx rgba(0, 0, 0, 0.1);
+	padding: 60rpx 70rpx;
+	.ai-logo {
+		display: block;
+		width: 172rpx;
+		height: 172rpx;
+		margin: 0 auto;
+	}
+	.ai-tip-title {
+		text-align: center;
+		font-size: 28rpx;
+		font-weight: bold;
+		line-height: 56rpx;
+		background-image: linear-gradient(90.00deg, rgba(13, 39, 247, 1),rgba(19, 193, 234, 1));
+		-webkit-background-clip: text;
+		color: transparent;
+		background-clip: text;
+		margin: 40rpx 0 32rpx;
+	}
+	.step-item {
+		color: rgba(1, 107, 246, 1);
+		font-size: 24rpx;
+		font-weight: 400;
+		line-height: 30rpx;
+		margin-bottom: 20rpx;
+	}
+	.step-file-img {
+		display: block;
+		width: 496rpx;
+		height: 82rpx;
+		margin-bottom: 20rpx;
+	}
+}
+
 // 简历解析提示弹窗
 .popup-container {
 	position: fixed;

+ 72 - 10
pages/my/index.vue

@@ -1,5 +1,6 @@
 <template>
 	<view class="my-center-page">
+		<global-push-ad />
 		<view class="my-bg">
 			<image src="@/static/images/my/icon/paopao.svg" mode="aspectFill" class="paopao paopao-one" />
 			<image src="@/static/images/my/icon/paopao.svg" mode="aspectFill" class="paopao paopao-two" />
@@ -113,7 +114,7 @@
 										@click="goNav('/pages/my/attachment')">
 										<!-- <image src="../../static/images/index/cpbk.svg" mode=""></image> -->
 										<image src="@/static/images/my/icons/file.png" mode=""></image>
-										<view class=""> 附件简历 </view>
+										<view class=""> 附件简历 </view>
 										<image src="@/static/images/my/ai-icon.png" class="ai-icon"></image>
 									</view>
 									<view class="jobServer-box-btn-item flex justify-center flex-wrap"
@@ -160,6 +161,10 @@
 								<image src="@/static/images/svg/edit.svg" class="function-icon"></image>
 								<text class="function-text">我要留言</text>
 							</view>
+							<view class="function-item" @click="goInvite" v-if="showInviteBtn">
+								<image src="@/static/images/svg/tuijianguan.svg" class="function-icon"></image>
+								<text class="function-text">推荐官</text>
+							</view>
 						</view>
 					</view>
 					<view class="beian">客服电话 400-000-0100 工作时间08:00-22:00</view>
@@ -329,6 +334,13 @@
 											我要留言
 										</view>
 									</view>
+									<view class="util-item" @click="goInvite" v-if="showInviteBtn">
+										<image src="@/static/images/svg/tuijianguan.svg"
+											style="width: 54rpx; height: 54rpx" mode=""></image>
+										<view class="" style="color: #1a1a1a; font-size: 24rpx; margin-top: 15rpx">
+											推荐官
+										</view>
+									</view>
 								</view>
 							</view>
 						</view>
@@ -383,6 +395,7 @@
 
 <script>
 import cModal from '@/components/c-modal.vue'
+import { openPushAd } from '@/common/pushAdStore.js'
 
 const rejectModalMessage = '您提交的申请被驳回,是否重新提交审核?'
 const registModalMessage = '您还未注册企业,是否立即注册企业?'
@@ -453,6 +466,8 @@ export default {
 			auditModal: false,
 			unverified: false,
 			modalMessage: '',
+			isRecommender: false, // 是否是推荐官
+			showInviteBtn: false, // 是否显示推荐官按钮
 		};
 	},
 	computed: {
@@ -588,9 +603,12 @@ export default {
 				this.openMsg();
 			}
 			// #endif
+
+			this.getNoticeData();
 		}
 		
 		this.getConfigData()
+		this.getRecommendationOfficerConfig()
 	},
 	methods: {
 		// 入职公司
@@ -1085,6 +1103,9 @@ export default {
 						}
 
 					}
+
+					// 判断是否是推荐官
+					this.isRecommender = res.data?.isRecommender == 1
 				}
 			});
 		},
@@ -1128,6 +1149,23 @@ export default {
 				this.noLogin();
 			}
 		},
+		// 跳转推荐官
+		goInvite() {
+			if (this.userId) {
+				// 判断是否是推荐官
+				if (this.isRecommender) {
+					uni.navigateTo({
+						url: '/pages/my/recommendationOfficer/list',
+					});
+				} else {
+					uni.navigateTo({
+						url: '/pages/my/recommendationOfficer/index',
+					});
+				}
+			} else {
+				this.noLogin();
+			}
+		},
 		//获取企业认证状态(1:审核中 2:通过 3:拒绝)
 		getCompanyStatus() {
 			this.$Request.get("/app/company/selectCompanyByUserId").then((res) => {
@@ -1478,8 +1516,30 @@ export default {
 					if (res.code == 0 && res.data?.length) {
 						this.showVipBanner = res.data[0]?.code == '开'
 					}
-					console.log(this.showVipBanner)
 				})
+		},
+		
+		// 获取推荐官配置数据
+		getRecommendationOfficerConfig() {
+			this.$Request.get('/app/dict/list', {
+				type: '推荐官'
+			}).then((res) => {
+				if (res.code == 0) {
+					this.showInviteBtn = res.data[0]?.code == '开'
+				}
+			})
+		},
+
+		// 获取公告数据
+		getNoticeData() {
+			this.$Request.get('/app/Login/pushNotice')
+			.then((res) => {
+				if (res.code == 0) {
+					openPushAd({
+						adImage: res.data?.url
+					})
+				}
+			})
 		}
 	},
 };
@@ -2029,14 +2089,14 @@ page {
 					width: 62rpx;
 					height: 62rpx;
 					margin-bottom: 10rpx;
-				}
-				
-				.ai-icon {
-					position: absolute;
-					top: -12rpx;
-					right: 12rpx;
-					width: 36rpx;
-					height: 36rpx;
+				}
+				
+				.ai-icon {
+					position: absolute;
+					top: -12rpx;
+					right: 12rpx;
+					width: 36rpx;
+					height: 36rpx;
 				}
 			}
 		}
@@ -2206,9 +2266,11 @@ page {
 			}
 
 			.function-text {
+				width: 96rpx;
 				color: #333;
 				font-size: 24rpx;
 				font-weight: 500;
+				text-align: center;
 			}
 		}
 	}

+ 205 - 0
pages/my/recommendationOfficer/index.vue

@@ -0,0 +1,205 @@
+<template>
+	<view class="page-container">
+		<nav-bar title="推荐官"></nav-bar>
+		<view class="content">
+			<image src="@/static/images/my/tuijianguan.png" class="people-img"></image>
+			<view class="title flex align-center justify-center">
+				<image src="@/static/images/my/agree.png" class="agree-icon"></image>
+				<text>金牌推荐官</text>
+			</view>
+			<view class="sub-title">成为亿职赞金牌推荐官,享超值权益</view>
+			
+			<view class="section">
+				<view class="section-title flex align-center justify-between">
+					<text>推荐人群:</text>
+					<view class="tag">跨境</view>
+				</view>
+				<view class="tip">跨境电商从业者</view>
+				<view class="tip">从业时间≥半年</view>
+				<view class="tip">平台运营或产品开发</view>
+			</view>
+			
+			<view class="section">
+				<view class="section-title flex align-center">
+					<text>推荐达标标准:</text>
+				</view>
+				<view class="tip">被推荐人成功注册</view>
+				<view class="tip">简历完善度>80%</view>
+			</view>
+			
+			<view class="section">
+				<view class="section-title flex align-center">
+					<text>超值权益:</text>
+				</view>
+				<view class="tip">成为推荐官,添加客服后即可获得10元奖励</view>
+				<view class="tip">推得越多,单价越高! 奖金次日微信红包秒发!</view>
+				<view class="tip">推1-10人:2元/人    (稳稳起步)</view>
+				<view class="tip">推11-30人:5元/人    (加速前进)</view>
+				<view class="tip">推31人以上:20元/人 (芜湖起飞!)</view>
+			</view>
+		</view>
+		
+		<view class="button-section fixed-section">
+			<view class="submit-btn" @click="handleBecomeOfficer">成为推荐官</view>
+		</view>
+		
+		<!-- 海报弹窗 -->
+		<Poster
+			:value="showPoster"
+			:rank="rank"
+			:posterBase64Url="posterBase64Url"
+			:posterMime="posterMime"
+			@close="handleClosePoster"
+			@saved="handleSaved"
+		></Poster>
+	</view>
+</template>
+
+<script>
+	import navBar from '@/components/nav-bar/index.vue';
+	import Poster from '@/components/poster/poster.vue'
+	
+	export default {
+		components: {
+			navBar,
+			Poster
+		},
+		data() {
+			return {
+				loading: false,
+				showPoster: false,
+				// 推荐官顺位
+				rank: 0,
+				posterBase64Url: ''
+			}
+		},
+		methods: {
+			// 成为推荐官
+			handleBecomeOfficer() {
+				this.loading = true
+				this.$queue.showLoading('成为推荐官中...')	
+				this.$Request.getT('/app/user/updateRecommender')
+				.then(res => {
+					if (res.code == 0) {
+						this.getPosterData()
+					} else {
+						this.$queue.hideLoading()
+					}
+				})
+				.catch(() => {
+					this.$queue.hideLoading()
+				})
+				.finally(() => {
+					this.loading = false
+				})
+			},
+
+			// 获取海报数据
+			getPosterData() {
+				this.$queue.showLoading('获取海报数据中...')
+				this.$Request.getT('/app/invite/getMyPoster')
+				.then(res => {
+					if (res.code == 0) {
+						this.showPoster = true
+						this.rank = res.data?.user?.recommenderRark ?? 0
+						this.posterBase64Url = res.data?.image ?? ''
+					}
+				}).finally(() => {
+					this.$queue.hideLoading()
+				})
+			},
+
+			// 关闭海报弹窗
+			handleClosePoster() {
+				this.showPoster = false
+				uni.redirectTo({
+					url: '/pages/my/recommendationOfficer/list'
+				})
+			},
+
+			handleSaved() {
+				uni.redirectTo({
+					url: '/pages/my/recommendationOfficer/list'
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import '@/common.scss';
+	
+	.page-container {
+		display: flex;
+		flex-direction: column;
+		height: 100vh;
+		font-family: DM Sans;
+		background-image: url('@/static/images/my/bg1.png');
+		background-size: 100% 100%;
+		
+		.content {
+			flex: 1;
+		}
+		
+		.people-img {
+			display: block;
+			width: 336rpx;
+			height: 336rpx;
+			margin: 0 auto;
+		}
+		.title {
+			margin: 18rpx 0 20rpx;
+			color: rgba(51, 51, 51, 1);
+			font-size: 36rpx;
+			font-weight: 500;
+			line-height: 46px;
+			.agree-icon {
+				width: 48rpx;
+				height: 48rpx;
+				margin-right: 8rpx;
+			}
+		}
+		.sub-title {
+			color: rgba(51, 51, 51, 1);
+			font-size: 28rpx;
+			font-weight: 500;
+			line-height: 36rpx;
+			text-align: center;
+			margin-bottom: 28rpx;
+		}
+		.section {
+			margin: 16rpx 20rpx;
+			background: #fff;
+			border-radius: 12rpx;
+			padding: 16rpx 36rpx;
+			.section-title {
+				color: rgba(51, 51, 51, 1);
+				font-weight: 500;
+				line-height: 36rpx;
+			}
+			.tag {
+				height: 32rpx;
+				font-size: 20rpx;
+				line-height: 1;
+				color: #fff;
+				border-radius: 8rpx;
+				background: linear-gradient(90.00deg, rgba(13, 39, 247, 0.75) 0%,rgba(19, 193, 234, 0.75) 100%);
+				padding: 6rpx 7rpx;
+				margin-top: 6rpx;
+				margin-left: 20rpx;
+				box-sizing: border-box;
+			}
+			.tip {
+				color: rgba(124, 125, 125, 1);
+				font-size: 24rpx;
+				font-weight: 400;
+				line-height: 40rpx;
+				margin-top: 12rpx;
+			}
+		}
+		.fixed-section {
+			box-shadow: none;
+			background: transparent;
+		}
+	}
+</style>

+ 755 - 0
pages/my/recommendationOfficer/list.vue

@@ -0,0 +1,755 @@
+<template>
+	<view class="page-container">
+		<nav-bar title="推荐官"></nav-bar>
+		
+		<view class="top-section">
+			<view class="flex align-center justify-between">
+				<view class="left flex align-center">
+					<image src="@/static/images/my/badge.png" class="badge-icon"></image>
+					<view>
+						<view class="name">金牌推荐官</view>
+						<view class="sub-name">{{ userName }}/{{ userPhone }}</view>
+					</view>
+				</view>
+				<view class="poster-btn" @click="getPosterData">获取专属推荐海报</view>
+			</view>
+			<view class="progress-wrapper">
+				<view class="progress-bar-wrapper">
+					<view class="progress-bar" :style="{ width: progressBarWidth }"></view>
+				</view>
+				<image src="@/static/images/my/badge2.png" class="badge badge1"></image>
+				<image src="@/static/images/my/badge2.png" class="badge badge2"></image>
+				<image src="@/static/images/my/badge2.png" class="badge badge3"></image>
+			</view>
+			<view class="names flex align-center">
+				<view class="name">推荐10人</view>
+				<view class="name">推荐20人</view>
+				<view class="name">推荐30人</view>
+			</view>
+		</view>
+		
+		<view class="toolbar-row flex align-center justify-between margin">
+			<view class="add-btn" @click="getWechatQrcode">点击添加企微兑现权益</view>
+			<view class="time-button flex align-center" @click="openDatePicker">
+				<text>{{ dateFilterText }}</text>
+				<u-icon name="arrow-down" size="24" style="margin-left: 2px;"></u-icon>
+			</view>
+		</view>
+		
+		<view class="list-wrapper margin">
+			<view class="tabs flex justify-center">
+				<view
+					class="tab-item"
+					:class="{ active: activeTab === item.value }"
+					v-for="item in tabs"
+					:key="item.value"
+					@click="switchTab(item.value)"
+				>
+					{{ item.name }}
+					<view class="border"></view>
+				</view>
+			</view>
+			
+			<scroll-view
+				class="list-scroll"
+				scroll-y
+				refresher-enabled
+				:refresher-threshold="80"
+				refresher-default-style="black"
+				:refresher-triggered="isRefreshing"
+				@refresherrefresh="handleRefresherRefresh"
+				@scrolltolower="handleScrollToLower"
+				:lower-threshold="80"
+			>
+				<view class="list-scroll-inner">
+				<view class="user-item flex justify-between" v-for="item in list" :key="item.user_id || item.id">
+					<view class="flex align-center">
+						<image :src="item.avatar || defaultAvatar" class="avatar"></image>
+						<view class="user-info">
+							<view class="user-name">{{ item.user_name }}</view>
+							<view class="user-tel">{{ item.phone }}</view>
+						</view>
+					</view>
+					<view class="time">{{ item.create_time }}</view>
+				</view>
+				<view v-if="!loadingMore && list.length === 0" class="list-empty">暂无数据</view>
+				<view v-if="loadingMore" class="list-load-tip">加载中...</view>
+				<view v-if="!hasMore && list.length > 0" class="list-nomore">— 没有更多了 —</view>
+				</view>
+			</scroll-view>
+		</view>
+		<view style="height: 20rpx;"></view>
+		
+		<u-popup v-model="showDatePickerPopup" mode="bottom" border-radius="20" :safeAreaInsetBottom="true">
+			<view class="date-picker-panel">
+				<view class="date-picker-header">
+					<text class="date-picker-btn" @click="showDatePickerPopup = false">取消</text>
+					<text class="date-picker-title">选择日期</text>
+					<text class="date-picker-btn date-picker-btn--primary" @click="confirmDatePicker">确定</text>
+				</view>
+				<!-- indicator 高度须与每行 date-picker-item 高度一致(px),否则滚轮视觉与 value 索引错位(微信官方说明) -->
+				<view class="date-picker-view-wrap">
+					<picker-view
+						v-if="showDatePickerPopup && yearList.length && dayList.length"
+						:key="pickerMountKey"
+						class="date-picker-view"
+						:indicator-style="pickerIndicatorStyle"
+						:value="datePickerValue"
+						@change="onDatePickerViewChange"
+					>
+						<picker-view-column>
+							<view v-for="(t, i) in modeColumn" :key="'mode-' + i" class="date-picker-item">
+								<text class="date-picker-item-text">{{ t }}</text>
+							</view>
+						</picker-view-column>
+						<picker-view-column>
+							<view v-for="(y, i) in yearList" :key="'y-' + i" class="date-picker-item">
+								<text class="date-picker-item-text">{{ y }}年</text>
+							</view>
+						</picker-view-column>
+						<picker-view-column>
+							<view v-for="(m, i) in monthList" :key="'mo-' + i" class="date-picker-item">
+								<text class="date-picker-item-text">{{ m }}月</text>
+							</view>
+						</picker-view-column>
+						<picker-view-column>
+							<view v-for="(d, i) in dayList" :key="'d-' + i" class="date-picker-item">
+								<text class="date-picker-item-text">{{ d }}日</text>
+							</view>
+						</picker-view-column>
+					</picker-view>
+					<!-- 全部:盖住后三列(H5 / APP-PLUS 用普通 view 即可) -->
+					<view
+						v-if="datePickerValue[0] === 0"
+						class="date-picker-ymd-mask"
+						@touchmove.stop.prevent="noop"
+					></view>
+				</view>
+			</view>
+		</u-popup>
+
+		<Poster
+			:showTitle="false"
+			:showTip="showTip"
+			:value="showPoster"
+			:posterBase64Url="posterBase64Url"
+			:saveBtnText="saveBtnText"
+			@close="showPoster = false"
+			@saved="handleSaved"
+		></Poster>
+	</view>
+</template>
+
+<script>
+	import navBar from '@/components/nav-bar/index.vue';
+	import Poster from '@/components/poster/poster.vue'
+	export default {
+		components: {
+			navBar,
+			Poster
+		},
+		data() {
+			return {
+				tabs: [
+					{
+						name: '已注册用户',
+						value: 0
+					},
+					{
+						name: '已达标用户',
+						value: 1
+					},
+				],
+				list: [],
+				defaultAvatar: '/static/logo.png',
+				showPoster: false,
+				activeTab: 0,
+				page: 1,
+				limit: 10,
+				totalPages: 1,
+				hasMore: true,
+				isRefreshing: false,
+				loadingMore: false,
+				/** 内容不足一屏时自动补页,防止无法触底 */
+				autoLoadMoreGuard: 0,
+				/** 每次发起列表请求递增,用于丢弃过期响应(避免刷新后仍被旧请求覆盖 hasMore/list) */
+				listReqId: 0,
+				/** 筛选日期:null 表示「全部」;否则为 YYYY-MM-DD */
+				filterDate: null,
+				showDatePickerPopup: false,
+				modeColumn: ['全部', '按日选择'],
+				yearList: [],
+				monthList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
+				dayList: [],
+				/** [模式索引, 年索引, 月索引, 日索引],与 picker-view 四列一一对应 */
+				datePickerValue: [0, 0, 0, 0],
+				/** 与下方 .date-picker-item 高度一致,单位 px */
+				pickerIndicatorStyle: 'height: 50px;',
+				/** 每次打开弹层递增,强制重建 picker-view,避免残留错位 */
+				pickerMountKey: 0,
+				userName: '',
+				userPhone: '',
+				inviteTotal: 0,
+				posterBase64Url: '',
+				loading: false,
+				posterType: 0, // 0: 推荐官海报, 1: 企微二维码
+			}
+		},
+		computed: {
+			dateFilterText() {
+				if (this.filterDate == null) return '全部'
+				return this.filterDate.replace(/-/g, '.')
+			},
+			showTip() {
+				return this.posterType === 0
+			},
+			saveBtnText() {
+				return this.posterType === 0 ? '保存海报并成为推荐官' : '保存企微二维码'
+			},
+			progressBarWidth() {
+				let percent = this.inviteTotal / 30
+				if (percent > 1) {
+					percent = 1
+				}
+				return percent * 100 + '%'
+			}
+		},
+		onLoad() {
+			const years = []
+			for (let y = 2010; y <= 2040; y++) years.push(y)
+			this.yearList = years
+		},
+		onShow() {
+			// 判断有没登录,如果没有登录则跳转登录
+			if (!uni.getStorageSync("userId")) {
+				this.$queue.toLogin()
+				return
+			}
+			this.getBasicInfo()
+			this.page = 1
+			this.getList()
+		},
+		methods: {
+			switchTab(value) {
+				if (this.activeTab === value) return
+				this.activeTab = value
+				this.page = 1
+				this.hasMore = true
+				this.getList()
+			},
+			/** scroll-view 下拉刷新(勿命名为 onPullDownRefresh,与 uni 页面生命周期重名会冲突) */
+			handleRefresherRefresh() {
+				this.loadingMore = false
+				this.isRefreshing = true
+				this.page = 1
+				this.hasMore = true
+				this.autoLoadMoreGuard = 0
+				this.getList()
+				this.getBasicInfo()
+			},
+			handleScrollToLower() {
+				if (!this.hasMore || this.loadingMore || this.isRefreshing) return
+				this.loadingMore = true
+				this.page += 1
+				this.getList()
+			},
+			/** 列表高度未超过 scroll-view 时不会触发 scrolltolower,自动再请求一页(最多 5 次) */
+			tryFillScrollArea() {
+				if (!this.hasMore || this.loadingMore || this.isRefreshing) return
+				if (this.autoLoadMoreGuard >= 5) return
+				this.$nextTick(() => {
+					setTimeout(() => {
+						const q = uni.createSelectorQuery().in(this)
+						q.select('.list-scroll').boundingClientRect()
+						q.select('.list-scroll-inner').boundingClientRect()
+						q.exec((res) => {
+							const scrollRect = res && res[0]
+							const innerRect = res && res[1]
+							if (!scrollRect || !innerRect) return
+							// 内容区高度未量到时不补页,避免误连请求
+							if (scrollRect.height > 20 && innerRect.height < 1) return
+							if (innerRect.height <= scrollRect.height + 4) {
+								this.autoLoadMoreGuard += 1
+								this.handleScrollToLower()
+							}
+						})
+					}, 100)
+				})
+			},
+			/** month 为自然月 1-12 */
+			getDaysInMonth(year, month) {
+				return new Date(year, month, 0).getDate()
+			},
+			updateDayList() {
+				let yi = Number(this.datePickerValue[1])
+				let mi = Number(this.datePickerValue[2])
+				yi = Math.min(Math.max(0, yi), this.yearList.length - 1)
+				mi = Math.min(Math.max(0, mi), this.monthList.length - 1)
+				const y = this.yearList[yi]
+				const m = this.monthList[mi]
+				const max = this.getDaysInMonth(y, m)
+				const days = []
+				for (let d = 1; d <= max; d++) days.push(d)
+				this.dayList = days
+			},
+			noop() {},
+			onDatePickerViewChange(e) {
+				const raw = e.detail.value
+				if (!raw || raw.length < 4) return
+				const val = raw.map((n) => Number(n))
+				const old = this.datePickerValue
+				// 全部:禁止年月日列变化(与遮罩配合;兜底)
+				if (old[0] === 0 && val[0] === 0 && (val[1] !== old[1] || val[2] !== old[2] || val[3] !== old[3])) {
+					this.datePickerValue = [0, old[1], old[2], old[3]]
+					return
+				}
+				this.datePickerValue = val
+				if (val[0] === 0) return
+				if (val[1] !== old[1] || val[2] !== old[2]) {
+					this.updateDayList()
+					this.$nextTick(() => {
+						const maxD = this.dayList.length
+						let di = val[3]
+						if (di >= maxD) di = Math.max(0, maxD - 1)
+						this.datePickerValue = [val[0], val[1], val[2], di]
+					})
+				}
+			},
+			openDatePicker() {
+				const t = new Date()
+				const ty = t.getFullYear()
+				const tm = t.getMonth() + 1
+				const td = t.getDate()
+				const yi = Math.max(0, this.yearList.indexOf(ty))
+				const moi = tm - 1
+				if (this.filterDate) {
+					const p = this.filterDate.split('-')
+					if (p.length === 3) {
+						const y = parseInt(p[0], 10)
+						const m = parseInt(p[1], 10)
+						const d = parseInt(p[2], 10)
+						const yIdx = Math.max(0, this.yearList.indexOf(y))
+						const mIdx = Math.min(Math.max(0, m - 1), 11)
+						this.datePickerValue = [1, yIdx, mIdx, 0]
+						this.updateDayList()
+						let di = this.dayList.indexOf(d)
+						if (di < 0) di = 0
+						di = Math.min(di, this.dayList.length - 1)
+						this.datePickerValue = [1, yIdx, mIdx, di]
+					} else {
+						this.datePickerValue = [0, yi, moi, 0]
+						this.updateDayList()
+						let di = this.dayList.indexOf(td)
+						if (di < 0) di = 0
+						di = Math.min(di, this.dayList.length - 1)
+						this.datePickerValue = [0, yi, moi, di]
+					}
+				} else {
+					this.datePickerValue = [0, yi, moi, 0]
+					this.updateDayList()
+					let di = this.dayList.indexOf(td)
+					if (di < 0) di = 0
+					di = Math.min(di, this.dayList.length - 1)
+					this.datePickerValue = [0, yi, moi, di]
+				}
+				this.pickerMountKey += 1
+				this.showDatePickerPopup = true
+			},
+			confirmDatePicker() {
+				const modeIdx = Number(this.datePickerValue[0])
+				let yi = Number(this.datePickerValue[1])
+				let moi = Number(this.datePickerValue[2])
+				let di = Number(this.datePickerValue[3])
+				yi = Math.min(Math.max(0, yi), this.yearList.length - 1)
+				moi = Math.min(Math.max(0, moi), 11)
+				di = Math.min(Math.max(0, di), Math.max(0, this.dayList.length - 1))
+				if (modeIdx === 0) {
+					this.filterDate = null
+				} else {
+					const y = this.yearList[yi]
+					const m = this.monthList[moi]
+					const d = this.dayList[di]
+					if (y != null && m != null && d != null) {
+						this.filterDate = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
+					}
+				}
+				this.showDatePickerPopup = false
+				this.page = 1
+				this.hasMore = true
+				this.getList()
+			},
+			getList() {
+				const expectPage = this.page
+				const reqId = ++this.listReqId
+				// 接口:inviteDate 为 yyyy.MM.dd,不传查全部
+				const inviteDate =
+					this.filterDate ? this.filterDate.replace(/-/g, '.') : ''
+				const params = {
+					page: expectPage,
+					limit: this.limit,
+					type: this.activeTab,
+					inviteDate
+				}
+				return this.$Request.get('/app/invite/selectInviteList', params)
+					.then((res) => {
+						if (reqId !== this.listReqId) return
+						if (expectPage !== this.page) return
+						if (res.code !== 0) return
+						const data = res.data || {}
+						const newList = data.list || []
+						const pageSize = Number(data.pageSize) || this.limit
+						const currPage = Number(data.currPage) || expectPage
+						let totalPage = Number(data.totalPage)
+						const totalCount = data.totalCount != null ? Number(data.totalCount) : null
+						if (Number.isNaN(totalPage) || totalPage < 1) {
+							if (totalCount != null && !Number.isNaN(totalCount) && pageSize > 0) {
+								totalPage = Math.max(1, Math.ceil(totalCount / pageSize))
+							} else {
+								totalPage = data.pages ?? data.pageTotal
+								totalPage = totalPage != null && !Number.isNaN(Number(totalPage))
+									? Math.max(1, Number(totalPage))
+									: 1
+							}
+						}
+						if (expectPage === 1) {
+							this.list = newList
+							this.autoLoadMoreGuard = 0
+						} else {
+							this.list = this.list.concat(newList)
+						}
+						this.totalPages = totalPage
+						// 优先用 totalCount 与已加载条数比较(与接口返回结构一致,最稳)
+						if (totalCount != null && !Number.isNaN(totalCount)) {
+							this.hasMore = this.list.length < totalCount
+						} else {
+							this.hasMore = currPage < totalPage
+						}
+						if (newList.length === 0 && expectPage > 1) this.hasMore = false
+					})
+					.catch(() => {
+						if (reqId !== this.listReqId) return
+						if (expectPage !== this.page) return
+						if (this.page > 1) this.page -= 1
+					})
+					.finally(() => {
+						if (reqId !== this.listReqId) return
+						this.isRefreshing = false
+						this.loadingMore = false
+						// 必须在 finally 之后:若放在 then 的 $nextTick 里,可能早于本处执行,isRefreshing 仍为 true,tryFillScrollArea 会直接 return(下拉刷新后无法补页/触底)
+						this.$nextTick(() => this.tryFillScrollArea())
+					})
+			},
+			// 获取基本信息
+			getBasicInfo() {
+				this.$Request.getT('/app/invite/selectInviteTotal').then((res) => {
+					if (res.code == 0) {
+						this.basicInfo = res.data
+						this.userName = res.data.userName
+						this.userPhone = res.data.phone
+						this.inviteTotal = res.data.totalCount
+					}
+				})
+			},
+			// 获取海报数据
+			getPosterData() {
+				if (this.loading) return
+				this.posterType = 0
+				this.loading = true
+				this.$queue.showLoading('获取海报数据中...')
+				this.$Request.getT('/app/invite/getMyPoster')
+				.then(res => {
+					if (res.code == 0) {
+						this.showPoster = true
+						this.posterBase64Url = res.data?.image ?? ''
+					}
+				}).finally(() => {
+					this.$queue.hideLoading()
+					this.loading = false
+				})
+			},
+			handleSaved() {
+				this.showPoster = false
+				// #ifndef H5
+				this.$queue.showToast('图片已保存到相册')
+				// #endif
+				// #ifdef H5
+				this.$queue.showToast('保存成功')
+				// #endif
+			},
+			// 获取企微二维码
+			getWechatQrcode() {
+				if (this.loading) return
+				this.posterType = 1
+				this.loading = true
+				this.$queue.showLoading('获取企微二维码中...')
+				this.$Request.getT('/app/common/type/830').then((res) => {
+					if (res.code == 0) {
+						this.posterBase64Url = res.data?.value ?? ''
+						this.showPoster = true
+					}
+				}).finally(() => {
+					this.$queue.hideLoading()
+					this.loading = false
+				})
+			},
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.page-container {
+		display: flex;
+		flex-direction: column;
+		height: 100vh;
+		/* 允许子项 flex:1 区域正确收缩,内部 scroll-view 才能滚 */
+		min-height: 0;
+		box-sizing: border-box;
+		font-family: DM Sans;
+		.top-section {
+			flex-shrink: 0;
+			margin: 24rpx 36rpx;
+			padding: 32rpx 28rpx 40rpx;
+			border-radius: 24rpx;
+			background-image: url('@/static/images/my/bg3.png'),
+				linear-gradient(90deg, #4a5cff 0%, #3d8aed 45%, #5ee7df 100%);
+			background-size: 100% 100%;
+			background-repeat: no-repeat;
+			background-position: center;
+			.badge-icon {
+				width: 80rpx;
+				height: 80rpx;
+				margin-right: 4rpx;
+			}
+			.name {
+				font-size: 36rpx;
+				line-height: 1;
+				color: #fff;
+			}
+			.sub-name {
+				font-size: 24rpx;
+				line-height: 36rpx;
+				color: #fff;
+				max-width: 240rpx;
+				overflow: hidden;
+				text-overflow: ellipsis;
+				white-space: nowrap;
+			}
+			.poster-btn {
+				color: rgba(255, 255, 255, 1);
+				font-style: Regular;
+				font-size: 14px;
+				font-weight: 400;
+				line-height: 24px;
+				padding: 0 30rpx;
+				border-radius: 30px;
+				background: rgba(255, 255, 255, 0.35);
+			}
+			.progress-wrapper {
+				position: relative;
+				margin: 80rpx auto 32rpx;
+				.progress-bar-wrapper {
+					width: 600rpx;
+					height: 20rpx;
+					border-radius: 20rpx;
+					background-color: rgba(255, 255, 255, 0.6);
+					margin: 0 auto;
+					.progress-bar {
+						height: 100%;
+						border-radius: 20rpx;
+						background: linear-gradient(90.00deg, rgba(27, 234, 228, 1) 0%,rgba(1, 107, 251, 1) 100%);
+					}
+				}
+				.badge {
+					position: absolute;
+					top: -22rpx;
+					width: 60rpx;
+					height: 60rpx;
+					transform: translateX(-100%);
+				}
+				.badge1 {
+					left: 34%;
+				}
+				.badge2 {
+					left: 66%;
+				}
+				.badge3 {
+					left: 100%;
+				}
+			}
+			.names {
+				.name {
+					flex: 1;
+					text-align: right;
+					color: rgba(255, 255, 255, 1);
+					font-size: 14px;
+					font-weight: 400;
+					line-height: 26px;
+				}				
+			}
+		}
+	}
+	.margin {
+		margin: 0 36rpx;
+	}
+	.toolbar-row {
+		flex-shrink: 0;
+	}
+	.add-btn {
+		color: rgba(1, 107, 246, 1);
+		font-size: 28rpx;
+		font-weight: 400;
+		line-height: 1;
+	}
+	.time-button {
+		box-sizing: border-box;
+		border: 0.5px solid rgba(1, 107, 246, 1);
+		border-radius: 4px;
+		background: rgba(153, 196, 250, 0.4);
+		padding: 8rpx;
+		color: rgba(1, 107, 246, 1);
+		font-size: 28rpx;
+		font-weight: 400;
+		line-height: 1;
+	}
+	.list-wrapper {
+		flex: 1;
+		min-height: 0;
+		/* 与内部 scroll-view 的 flex:1;height:0 配合,否则部分端无法滚动、触底不触发 */
+		height: 0;
+		display: flex;
+		flex-direction: column;
+		overflow: hidden;
+		border-radius: 12rpx;
+		box-shadow: 0 0 20rpx rgba(0, 0, 0, 0.1);
+		padding: 20rpx 0 40rpx;
+		margin-top: 32rpx;
+		box-sizing: border-box;
+		.tabs {
+			flex-shrink: 0;
+			height: 72rpx;
+			gap: 40rpx;
+			.tab-item {
+				position: relative;
+				color: rgba(102, 102, 102, 1);
+				font-size: 28rpx;
+				font-weight: 500;
+				line-height: 32rpx;
+				.border {
+					position: absolute;
+					left: 50%;
+					bottom: 0;
+					width: 80rpx;
+					height: 4rpx;
+					background-color: #fff;
+					transform: translateX(-50%);
+					border-radius: 4rpx 4rpx 0 0;
+				}
+			}
+			.active {
+				color: #016BF6;
+				.border {
+					background: #016BF6;
+				}
+			}
+		}
+
+		.list-scroll {
+			flex: 1;
+			height: 0;
+			width: 100%;
+		}
+		
+		.user-item {
+			margin: 28rpx 36rpx 0;
+			.avatar {
+				width: 50rpx;
+				height: 50rpx;
+				border-radius: 50%;
+				margin-right: 24rpx;
+				border: 1px solid #eee;
+			}
+			.user-info {
+				color: rgba(51, 51, 51, 1);
+				font-size: 28rpx;
+				line-height: 32rpx;
+				.user-name {
+					max-width: 300rpx;
+					overflow: hidden;
+					text-overflow: ellipsis;
+					white-space: nowrap;
+				}
+			}
+			.time {
+				height: 36rpx;
+				color: rgba(153, 153, 153, 1);
+				font-size: 24rpx;
+				font-weight: 400;
+			}
+		}
+		.list-empty,
+		.list-load-tip,
+		.list-nomore {
+			text-align: center;
+			padding: 32rpx;
+			font-size: 26rpx;
+			color: #999;
+		}
+	}
+
+	.date-picker-panel {
+		background: #fff;
+		padding-bottom: constant(safe-area-inset-bottom);
+		padding-bottom: env(safe-area-inset-bottom);
+	}
+	.date-picker-header {
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		padding: 24rpx 32rpx;
+		border-bottom: 1rpx solid #eee;
+	}
+	.date-picker-title {
+		font-size: 32rpx;
+		font-weight: 500;
+		color: #303133;
+	}
+	.date-picker-btn {
+		font-size: 30rpx;
+		color: #606266;
+		padding: 8rpx 12rpx;
+	}
+	.date-picker-btn--primary {
+		color: #2979ff;
+	}
+	.date-picker-view-wrap {
+		position: relative;
+		width: 100%;
+		min-height: 250px;
+	}
+	.date-picker-ymd-mask {
+		position: absolute;
+		left: 25%;
+		top: 0;
+		width: 75%;
+		height: 100%;
+		z-index: 2;
+		background: rgba(0, 0, 0, 0.06);
+	}
+	.date-picker-view {
+		width: 100%;
+		/* 5 行 × 50px,与 uni-datetime-picker 一致,避免半行偏移 */
+		height: 250px;
+	}
+	.date-picker-item {
+		height: 50px;
+		line-height: 50px;
+		text-align: center;
+		box-sizing: border-box;
+	}
+	.date-picker-item-text {
+		font-size: 16px;
+		color: #303133;
+		line-height: 50px;
+		height: 50px;
+	}
+</style>

+ 9 - 16
pages/public/improvePrompt.vue

@@ -21,13 +21,8 @@
 			</view>
 		</view>
 		<view class="buttons">
-			<template v-if="scene == 1">
-				<view class="white-btn" @click="toOnlineResume">完善在线简历</view>
-			</template>
-			<template v-else>
-				<view class="link-btn" @click="handleSubmit">手动完善简历</view>
-				<view class="white-btn" @click="showTipPopup = true">附件简历一键识别</view>
-			</template>
+			<view class="link-btn" @click="handleSubmit">手动完善简历</view>
+			<view class="white-btn" @click="showTipPopup = true">附件简历一键识别</view>
 		</view>
 		
 		<view class="load-container" v-if="showLoadCard">
@@ -78,12 +73,7 @@
 		// 	}
 		// },
 		methods: {
-			toOnlineResume() {
-				uni.navigateTo({
-					url: `/pages/my/onlineResume?scene=${this.scene}`
-				})
-			},
-			// 手动完善
+			// 完善简历
 			handleSubmit() {
 				uni.navigateTo({
 					url: `/pages/my/onlineResume?scene=${this.scene}`
@@ -154,9 +144,12 @@
 					if (res.code == 0 && res.data) {
 						this.$queue.setData('AI_Resume_Data', res.data)
 						
-						uni.reLaunch({
-							url: '/pages/my/onlineResume?fileId=AI_Resume_Data'
-						})
+						let url = '/pages/my/onlineResume?fileId=AI_Resume_Data'
+						if (this.scene == 1) {
+							url = `${url}&scene=${this.scene}`
+						}
+						
+						uni.reLaunch({ url })
 					} else {
 						this.showModal(res?.msg || '解析失败,请稍后重试')
 					}

+ 13 - 15
pages/public/invite/index.vue

@@ -1,12 +1,10 @@
 <template>
 	<view class="container">
-		<image src="/static/images/logo-long.png" class="logo"></image>
-		<image src="/static/images/invite.png" class="main-img"></image>
-		<view class="company-info">
-			<view class="name">{{ userName }}</view>
-			<view class="company-name">{{ companyName }}</view>
+		<view>
+			<image src="/static/images/logo-long.png" class="logo"></image>
+			<image src="/static/images/invite.png" class="main-img"></image>			
 		</view>
-		<view class="tip">诚邀您加入亿职赞</view>
+		<view class="tip">欢迎加入亿职赞</view>
 		<u-button class="button" shape="circle" :hair-line="false" @click="join">立即加入</u-button>
 	</view>
 </template>
@@ -24,19 +22,16 @@
 			if (options.inviteCode) {
 				this.inviteCode = decodeURIComponent(options.inviteCode)
 			}
-			if (options.userName) {
-				this.userName = decodeURIComponent(options.userName)
-			}
-			if (options.companyName) {
-				this.companyName = decodeURIComponent(options.companyName)
-			}
 		},
 		methods: {
 			join() {
 				// type: 2.手机验证码登录
 				// scene: 1.H5邀请
+				// uni.navigateTo({
+				// 	url: `/pages/public/loginphone?type=2&scene=1&inviteCode=${this.inviteCode}`
+				// })
 				uni.navigateTo({
-					url: `/pages/public/loginphone?type=2&scene=1&inviteCode=${this.inviteCode}`
+					url: `/pages/public/loginV2?type=2&scene=1&inviteCode=${this.inviteCode}`
 				})
 			}
 		}
@@ -45,7 +40,11 @@
 
 <style lang="scss">
 	.container {
-		padding: 72rpx 30rpx 0;
+		display: flex;
+		flex-direction: column;
+		justify-content: space-between;
+		height: 100vh;
+		padding: 72rpx 30rpx 200rpx;
 	}
 	.logo {
 		display: block;
@@ -78,7 +77,6 @@
 	.tip {
 		text-align: center;
 		margin-bottom: 100rpx;
-		font-weight: bold;
 		font-size: 48rpx;
 	}
 	.button {

+ 28 - 4
pages/public/loginV2.vue

@@ -69,6 +69,8 @@
 				timer: null,
 				disabled: false,
 				loading: false,
+				inviteCode: '',
+				scene: 0, // 0 默认,1 H5邀请
 				// isFirst: false,
 				// show: false,
 				// currentType: 1,
@@ -96,6 +98,14 @@
 			this.checkLoginStatus()
 			// #endif
 			
+			if (options.scene) {
+				this.scene = options.scene
+			}
+			
+			if (options.inviteCode) {
+				this.inviteCode = options.inviteCode
+			}
+			
 			// // 判断是否是第一次
 			// if (options.isFirst) {
 			// 	this.isFirst = true
@@ -199,9 +209,16 @@
 				uni.showLoading({
 					title: '登录中'
 				})
+				
+				// 邀请参数
+				let params = {}
+				if (this.inviteCode) {
+					params['inviterCode'] = this.inviteCode
+				}
+				
 				let platform = 'app'
 				// #ifdef H5
-					platform = 'h5'
+					platform = 'H5'
 				// #endif
 				// 获取极光推送登录信息
 				let JPLoginParams = getJPLoginParams()
@@ -209,14 +226,16 @@
 					phone: this.phoneNum,
 					msg: this.verifyCode,
 					platform,
-					...JPLoginParams
+					...JPLoginParams,
+					...params
 				})
 				this.$Request
 					.post('/app/Login/registerAndLogin', {
 						phone: this.phoneNum,
 						msg: this.verifyCode,
 						platform,
-						...JPLoginParams
+						...JPLoginParams,
+						...params
 					})
 					.then((res) => {
 						this.loginFinished(res)
@@ -273,7 +292,12 @@
 					this.$queue.setData("invitationCode", res.user.invitationCode)
 					this.$queue.setData("inviterCode", res.user.inviterCode)
 					this.getIsVip()
-					if (res.user.userType) {
+					if (this.inviteCode) {
+						// H5邀请
+						uni.redirectTo({
+							url: `/package/jobIntention/basicInfo?scene=${this.scene}`
+						});
+					} else if (res.user.userType) {
 						this.$queue.setData("userType", res.user.userType)
 						uni.reLaunch({
 							url: '/pages/my/index',

BIN
static/images/my/agree.png


BIN
static/images/my/ai-logo.png


BIN
static/images/my/badge.png


BIN
static/images/my/badge2.png


BIN
static/images/my/bg1.png


BIN
static/images/my/bg3.png


BIN
static/images/my/resume-file.png


BIN
static/images/my/tuijianguan.png


File diff suppressed because it is too large
+ 1 - 0
static/images/svg/tuijianguan.svg


Some files were not shown because too many files changed in this diff