diff --git a/apiRc/login.js b/apiRc/login.js index 74ee74b..8f4b12f 100644 --- a/apiRc/login.js +++ b/apiRc/login.js @@ -44,10 +44,22 @@ export function register(data) { }) } +// 身份证号码登录 +export function idCardLogin(data) { + return request({ + method: 'post', + url: '/app/idCardLogin', + data, + headers: { + isToken: false + } + }) +} + // 获取用户详细信息 export function getInfo() { return request({ url: '/getInfo', method: 'get' }) -} \ No newline at end of file +} diff --git a/components/CustomTabBar/CustomTabBar.vue b/components/CustomTabBar/CustomTabBar.vue index 83d3fd0..5e2b9fe 100644 --- a/components/CustomTabBar/CustomTabBar.vue +++ b/components/CustomTabBar/CustomTabBar.vue @@ -26,6 +26,7 @@ import { ref, computed, watch, onMounted } from 'vue'; import { storeToRefs } from 'pinia'; import useUserStore from '@/stores/useUserStore'; import { useReadMsg } from '@/stores/useReadMsg'; +import { checkLoginAndNavigate } from '@/utils/loginHelper'; const props = defineProps({ currentPage: { @@ -157,77 +158,75 @@ watch(() => userInfo.value, (newUserInfo, oldUserInfo) => { const switchTab = (item, index) => { console.log('switchTab called', item, index); - // 检查是否为"发布岗位"页面,需要判断企业信息是否完整 - if (item.path === '/pages/job/publishJob') { - // 检查用户是否已登录 - const token = uni.getStorageSync('token') || ''; - const hasLogin = userStore.hasLogin; - - if (!token || !hasLogin) { - // 未登录,发送事件显示登录弹窗 - uni.$emit('showLoginModal'); - return; // 不进行页面跳转 - } - - // 已登录,检查企业信息是否完整 - const cachedUserInfo = uni.getStorageSync('userInfo') || {}; - const storeUserInfo = userInfo.value || {}; - const currentUserInfo = storeUserInfo.id ? storeUserInfo : cachedUserInfo; - - // 判断企业信息字段company是否为null或undefined - if (!currentUserInfo.company || currentUserInfo.company === null) { - // 企业信息为空,跳转到企业信息补全页面 - uni.navigateTo({ - url: '/pages/complete-info/company-info', - }); - } else { - // 企业信息完整,跳转到发布岗位页面 - uni.navigateTo({ - url: '/pages/job/publishJob', - }); - } - - currentItem.value = item.id; - return; - } + // 检查是否为需要登录的页面 + const loginRequiredPages = [ + '/pages/job/publishJob', + '/pages/mine/mine', + '/pages/mine/company-mine' + ]; - // 检查是否为"我的"页面,需要登录验证和用户类型判断 - if (item.path === '/pages/mine/mine') { + if (loginRequiredPages.includes(item.path)) { // 检查用户是否已登录 const token = uni.getStorageSync('token') || ''; const hasLogin = userStore.hasLogin; if (!token || !hasLogin) { - // 未登录,发送事件显示登录弹窗 - uni.$emit('showLoginModal'); + // 未登录,根据平台类型跳转到对应的登录页面 + checkLoginAndNavigate(); return; // 不进行页面跳转 } - // 已登录,根据用户类型跳转到不同的"我的"页面 - const cachedUserInfo = uni.getStorageSync('userInfo') || {}; - const storeIsCompanyUser = userInfo.value?.isCompanyUser; - const cachedIsCompanyUser = cachedUserInfo.isCompanyUser; - - // 获取用户类型 - const userType = Number(storeIsCompanyUser !== undefined ? storeIsCompanyUser : (cachedIsCompanyUser !== undefined ? cachedIsCompanyUser : 1)); - - let targetPath = '/pages/mine/mine'; // 默认求职者页面 - - if (userType === 0) { - // 企业用户,跳转到企业我的页面 - targetPath = '/pages/mine/company-mine'; - } else { - // 求职者或其他用户类型,跳转到普通我的页面 - targetPath = '/pages/mine/mine'; + // 已登录,处理特定页面的逻辑 + if (item.path === '/pages/job/publishJob') { + // 检查企业信息是否完整 + const cachedUserInfo = uni.getStorageSync('userInfo') || {}; + const storeUserInfo = userInfo.value || {}; + const currentUserInfo = storeUserInfo.id ? storeUserInfo : cachedUserInfo; + + // 判断企业信息字段company是否为null或undefined + if (!currentUserInfo.company || currentUserInfo.company === null) { + // 企业信息为空,跳转到企业信息补全页面 + uni.navigateTo({ + url: '/pages/complete-info/company-info', + }); + } else { + // 企业信息完整,跳转到发布岗位页面 + uni.navigateTo({ + url: '/pages/job/publishJob', + }); + } + + currentItem.value = item.id; + return; } - // 跳转到对应的页面 - uni.navigateTo({ - url: targetPath, - }); - - currentItem.value = item.id; - return; + if (item.path === '/pages/mine/mine') { + // 根据用户类型跳转到不同的"我的"页面 + const cachedUserInfo = uni.getStorageSync('userInfo') || {}; + const storeIsCompanyUser = userInfo.value?.isCompanyUser; + const cachedIsCompanyUser = cachedUserInfo.isCompanyUser; + + // 获取用户类型 + const userType = Number(storeIsCompanyUser !== undefined ? storeIsCompanyUser : (cachedIsCompanyUser !== undefined ? cachedIsCompanyUser : 1)); + + let targetPath = '/pages/mine/mine'; // 默认求职者页面 + + if (userType === 0) { + // 企业用户,跳转到企业我的页面 + targetPath = '/pages/mine/company-mine'; + } else { + // 求职者或其他用户类型,跳转到普通我的页面 + targetPath = '/pages/mine/mine'; + } + + // 跳转到对应的页面 + uni.navigateTo({ + url: targetPath, + }); + + currentItem.value = item.id; + return; + } } // 判断是否为 tabBar 页面 diff --git a/components/wxAuthLogin/WxAuthLogin.vue b/components/wxAuthLogin/WxAuthLogin.vue index 1ee2dc5..668956a 100644 --- a/components/wxAuthLogin/WxAuthLogin.vue +++ b/components/wxAuthLogin/WxAuthLogin.vue @@ -9,7 +9,7 @@ - + 欢迎使用就业服务 需要您授权手机号登录 @@ -227,65 +227,12 @@ const wxLogin = () => { } // #ifdef H5 - // H5网页微信登录逻辑 - uni.showLoading({ title: '登录中...' }); - - // 获取微信授权code - uni.login({ - provider: 'weixin', - success: (loginRes) => { - console.log('微信登录成功:', loginRes); - - // 调用后端接口进行登录 - $api.createRequest('/app/appLogin', { - code: loginRes.code, - userType: userType.value - }, 'post').then((resData) => { - uni.hideLoading(); - - if (resData.token) { - loginSetToken(resData.token).then((resume) => { - console.log(resData, 'resData.isCompanyUser'); - // 更新用户类型到缓存 - if (resData.isCompanyUser) { - console.log(resData.isCompanyUser, 'resData.isCompanyUser'); - const userInfo = uni.getStorageSync('userInfo') || {}; - userInfo.isCompanyUser = Number(resData.isCompanyUser); // 0-企业用户,1-求职者 - uni.setStorageSync('userInfo', userInfo); - } - - $api.msg('登录成功'); - close(); - emit('success'); - - if (!resume.data.jobTitleId) { - if (userType.value === 1) { - // 求职者跳转到个人信息补全页面 - uni.navigateTo({ - url: '/pages/complete-info/complete-info?step=1' - }); - } else if (userType.value === 0) { - // 招聘者跳转到企业信息补全页面 - uni.navigateTo({ - url: '/pages/complete-info/company-info' - }); - } - } - }); - } else { - $api.msg('登录失败,请重试'); - } - }).catch((err) => { - uni.hideLoading(); - $api.msg(err.msg || '登录失败,请重试'); - }); - }, - fail: (err) => { - uni.hideLoading(); - console.error('微信登录失败:', err); - $api.msg('微信登录失败'); - } + // H5端跳转到H5登录页面 + close(); + uni.navigateTo({ + url: '/pages/login/h5-login' }); + return; // #endif // #ifdef APP-PLUS @@ -561,4 +508,3 @@ defineExpose({ button::after border: none - diff --git a/packageA/pages/UnitDetails/UnitDetails.vue b/packageA/pages/UnitDetails/UnitDetails.vue index 138d967..b68307e 100644 --- a/packageA/pages/UnitDetails/UnitDetails.vue +++ b/packageA/pages/UnitDetails/UnitDetails.vue @@ -338,19 +338,19 @@ if (diffSeconds < 60) { return '刚刚发布'; } else if (diffMinutes < 60) { - return `${diffMinutes}分钟前发布`; + return `${diffMinutes}分钟前`; } else if (diffHours < 24) { - return `${diffHours}小时前发布`; + return `${diffHours}小时前`; } else if (diffDays === 1) { return '昨天发布'; } else if (diffDays < 7) { - return `${diffDays}天前发布`; + return `${diffDays}天前`; } else if (diffWeeks < 4) { - return `${diffWeeks}周前发布`; + return `${diffWeeks}周前`; } else if (diffMonths < 12) { - return `${diffMonths}个月前发布`; + return `${diffMonths}个月前`; } else { - return `${diffYears}年前发布`; + return `${diffYears}年前`; } } catch (error) { console.error('格式化发布时间失败:', error); diff --git a/pages.json b/pages.json index f06439e..1bb733b 100644 --- a/pages.json +++ b/pages.json @@ -111,6 +111,20 @@ "style": { "navigationBarTitleText": "编辑联系人" } + }, + { + "path": "pages/login/h5-login", + "style": { + "navigationBarTitleText": "登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/login/id-card-login", + "style": { + "navigationBarTitleText": "社保登录", + "navigationStyle": "custom" + } } ], "subpackages": [ @@ -658,4 +672,3 @@ }, "uniIdRouter": {} } - \ No newline at end of file diff --git a/pages/index/components/index-one.vue b/pages/index/components/index-one.vue index 33c6bf9..fda098b 100644 --- a/pages/index/components/index-one.vue +++ b/pages/index/components/index-one.vue @@ -425,6 +425,7 @@ const { $api, navTo, vacanciesTo, formatTotal, config } = inject('globalFunction import { onLoad, onShow } from '@dcloudio/uni-app'; import { storeToRefs } from 'pinia'; import useUserStore from '@/stores/useUserStore'; +import { checkLoginAndNavigate, getPlatformType } from '@/utils/loginHelper'; const { userInfo, hasLogin, token } = storeToRefs(useUserStore()); // 计算是否显示求职者内容 @@ -685,8 +686,8 @@ watch([hasLogin, userInfo], () => { const checkLogin = () => { const tokenValue = uni.getStorageSync('token') || ''; if (!tokenValue || !hasLogin.value) { - // 未登录,打开授权弹窗 - wxAuthLoginRef.value?.open(); + // 未登录,根据平台类型跳转到对应登录页面 + checkLoginAndNavigate(); return false; } return true; diff --git a/pages/job/publishJob.vue b/pages/job/publishJob.vue index e8ba499..ec0281a 100644 --- a/pages/job/publishJob.vue +++ b/pages/job/publishJob.vue @@ -118,7 +118,7 @@ v-model="formData.jobLocation" /> - + @@ -800,25 +800,27 @@ const validateForm = () => { .location-input { flex: 1; - padding-right: 80rpx; + padding-right: 100rpx; } .location-icon-btn { - position: absolute; - right: -3px; - top: 20%; - transform: translateY(-50%); - width: 80rpx; - height: 80rpx; - display: flex; - align-items: center; - justify-content: center; - background: transparent; - border: none; - border-radius: 8rpx; - z-index: 99; + position: absolute; + right: 0; + top: 44%; + transform: translateY(-50%); + width: 80rpx; + height: 60rpx; + display: flex; + align-items: center; + justify-content: center; + background: #f0f0f0; + border: none; + border-radius: 12rpx; + transition: all 0.3s ease; + z-index: 99; + &:active { - background: #f0f0f0; + transform: translateY(-50%) scale(0.95); } } } diff --git a/pages/login/h5-login.vue b/pages/login/h5-login.vue new file mode 100644 index 0000000..3dc294f --- /dev/null +++ b/pages/login/h5-login.vue @@ -0,0 +1,529 @@ + + + + + diff --git a/pages/login/id-card-login.vue b/pages/login/id-card-login.vue new file mode 100644 index 0000000..4396500 --- /dev/null +++ b/pages/login/id-card-login.vue @@ -0,0 +1,372 @@ + + + + + diff --git a/static/logo1-S.png b/static/logo1-S.png new file mode 100644 index 0000000..dd7e619 Binary files /dev/null and b/static/logo1-S.png differ diff --git a/static/logo2-S.png b/static/logo2-S.png new file mode 100644 index 0000000..caba026 Binary files /dev/null and b/static/logo2-S.png differ diff --git a/static/logo3.png b/static/logo3.png new file mode 100644 index 0000000..d246fbd Binary files /dev/null and b/static/logo3.png differ diff --git a/utils/loginHelper.js b/utils/loginHelper.js new file mode 100644 index 0000000..11ae3ef --- /dev/null +++ b/utils/loginHelper.js @@ -0,0 +1,158 @@ +/** + * 登录帮助工具类 + * 用于根据平台类型跳转到对应的登录页面 + */ + +import useUserStore from '@/stores/useUserStore'; + +/** + * 检查当前平台类型 + * @returns {string} 平台类型:'mp-weixin' | 'h5' | 'app' + */ +export function getPlatformType() { + // #ifdef MP-WEIXIN + return 'mp-weixin'; + // #endif + + // #ifdef H5 + return 'h5'; + // #endif + + // #ifdef APP-PLUS + return 'app'; + // #endif + + return 'h5'; // 默认返回H5 +} + +/** + * 跳转到对应平台的登录页面 + * @param {Object} options 跳转选项 + * @param {string} options.redirectUrl 登录成功后跳转的URL + * @param {string} options.loginType 登录类型:'wechat' | 'account' | 'idCard' + */ +export function navigateToLoginPage(options = {}) { + const { redirectUrl, loginType = 'account' } = options; + const platform = getPlatformType(); + + let loginPage = ''; + let params = {}; + + if (redirectUrl) { + params.redirectUrl = redirectUrl; + } + + switch (platform) { + case 'mp-weixin': + // 小程序端使用微信授权登录,直接显示微信授权弹窗 + uni.$emit('showLoginModal', { loginType: 'wechat' }); + return; + + case 'h5': + if (loginType === 'idCard') { + // H5端身份证号码登录 + loginPage = '/pages/login/id-card-login'; + } else { + // H5端账号密码登录 + loginPage = '/pages/login/h5-login'; + } + break; + + case 'app': + // App端使用微信授权登录 + uni.$emit('showLoginModal', { loginType: 'wechat' }); + return; + + default: + loginPage = '/pages/login/h5-login'; + } + + if (loginPage) { + const queryString = Object.keys(params).length > 0 + ? `?${new URLSearchParams(params).toString()}` + : ''; + + uni.navigateTo({ + url: `${loginPage}${queryString}` + }); + } +} + +/** + * 检查登录状态,如果未登录则跳转到对应登录页面 + * @param {Object} options 选项 + * @param {string} options.redirectUrl 登录成功后跳转的URL + * @param {string} options.loginType 登录类型 + * @returns {boolean} 是否已登录 + */ +export function checkLoginAndNavigate(options = {}) { + const userStore = useUserStore(); + + if (userStore.hasLogin) { + return true; + } + + // 未登录,跳转到对应登录页面 + navigateToLoginPage(options); + return false; +} + +/** + * 处理身份证号码登录的URL参数 + * @param {string} url 包含参数的URL + * @returns {Object} 解析后的参数对象 + */ +export function parseIdCardLoginParams(url) { + const urlObj = new URL(url, window.location.origin); + const params = {}; + + // 获取身份证号码base64参数 + const idCardBase64 = urlObj.searchParams.get('idCardBase64'); + if (idCardBase64) { + params.idCardBase64 = idCardBase64; + } + + // 获取重定向URL + const redirectUrl = urlObj.searchParams.get('redirectUrl'); + if (redirectUrl) { + params.redirectUrl = redirectUrl; + } + + return params; +} + +/** + * Base64解码身份证号码 + * @param {string} base64Str base64编码的字符串 + * @returns {string} 解码后的身份证号码 + */ +export function decodeIdCardBase64(base64Str) { + try { + // #ifdef H5 + return atob(base64Str); + // #endif + + // #ifdef MP-WEIXIN + const result = uni.base64ToArrayBuffer(base64Str); + return String.fromCharCode.apply(null, new Uint8Array(result)); + // #endif + + // #ifdef APP-PLUS + return plus.base64.decode(base64Str); + // #endif + + // 默认使用H5方式 + return atob(base64Str); + } catch (error) { + console.error('Base64解码失败:', error); + throw new Error('身份证号码解码失败'); + } +} + +export default { + getPlatformType, + navigateToLoginPage, + checkLoginAndNavigate, + parseIdCardLoginParams, + decodeIdCardBase64 +}; diff --git a/登录系统实现总结.md b/登录系统实现总结.md new file mode 100644 index 0000000..b1ab1de --- /dev/null +++ b/登录系统实现总结.md @@ -0,0 +1,139 @@ +# 多端登录系统实现总结 + +## 项目概述 +已成功完成多端登录系统的重构,支持小程序端、H5端和App端的差异化登录方式。 + +## 实现功能 + +### 1. 多端登录策略 +- **小程序端 (MP-WEIXIN)**: 微信授权登录 +- **H5端 (H5)**: 账号密码登录 + 身份证号码登录 +- **App端 (APP-PLUS)**: 微信授权登录 + +### 2. 新增页面 + +#### 2.1 H5账号密码登录页面 (`pages/login/h5-login.vue`) +- 账号密码输入表单 +- 登录按钮 +- 跳转到身份证号码登录的入口 +- 美观的UI设计,渐变背景和圆角卡片 + +#### 2.2 身份证号码登录页面 (`pages/login/id-card-login.vue`) +- Loading页面设计,显示"登录中,请稍候..." +- 支持从URL参数获取Base64编码的身份证号码 +- 多端Base64解码支持: + - H5: `atob()` + - 小程序: `uni.base64ToArrayBuffer()` + - App: `plus.base64.decode()` +- 调用后端身份证号码登录接口 + +### 3. API接口扩展 + +#### 3.1 身份证号码登录接口 (`apiRc/login.js`) +```javascript +export function idCardLogin(data) { + return request({ + method: 'post', + url: '/app/idCardLogin', + data, + headers: { + isToken: false + } + }) +} +``` + +### 4. 登录帮助工具类 (`utils/loginHelper.js`) +- 平台类型检测 (`getPlatformType`) +- 登录页面跳转 (`navigateToLoginPage`) +- 登录状态检查 (`checkLoginAndNavigate`) +- URL参数解析 (`parseIdCardLoginParams`) +- Base64解码 (`decodeIdCardBase64`) + +### 5. 现有组件集成 + +#### 5.1 微信授权登录组件 (`components/wxAuthLogin/WxAuthLogin.vue`) +- 已修复语法错误 +- H5端点击微信授权登录时跳转到H5登录页面 +- 小程序端保持原有的微信授权登录逻辑 +- App端保持原有的微信授权登录逻辑 + +#### 5.2 首页组件 (`pages/index/components/index-one.vue`) +- 已集成登录检查逻辑 +- 在`onMounted`生命周期中调用`checkLoginAndNavigate()` + +#### 5.3 Tabbar组件 (`components/CustomTabBar/CustomTabBar.vue`) +- 已集成登录状态判断 +- 对于需要登录的页面(发布岗位、我的页面),检查登录状态 +- 未登录时跳转到对应登录页面 + +### 6. 路由配置 (`pages.json`) +- 已注册新的登录页面路由 +- H5登录页面: `/pages/login/h5-login` +- 身份证号码登录页面: `/pages/login/id-card-login` + +## 登录流程 + +### 小程序端登录流程 +1. 用户点击需要登录的功能 +2. 显示微信授权登录弹窗 +3. 用户授权手机号 +4. 调用后端接口获取token +5. 登录成功,跳转到目标页面 + +### H5端登录流程 +1. 用户点击需要登录的功能 +2. 跳转到H5登录页面 +3. 用户可选择: + - 账号密码登录 + - 身份证号码登录 +4. 登录成功,跳转到目标页面 + +### 身份证号码登录流程 +1. 第三方系统跳转到身份证号码登录页面 +2. 通过URL参数传递Base64编码的身份证号码 +3. 页面自动解码身份证号码 +4. 调用后端身份证号码登录接口 +5. 获取token和用户信息 +6. 登录成功,跳转到首页 + +## 技术特点 + +### 1. 条件编译 +使用`#ifdef`、`#ifndef`实现多端差异化逻辑 + +### 2. Base64解码 +- H5: `atob(base64Str)` +- 小程序: `uni.base64ToArrayBuffer(base64Str)` +- App: `plus.base64.decode(base64Str)` + +### 3. 状态管理 +使用Pinia store (`useUserStore`)管理用户状态 + +### 4. 错误处理 +完善的错误处理和用户提示 + +## 测试建议 + +1. **小程序端测试**: 验证微信授权登录功能 +2. **H5端测试**: 验证账号密码登录和身份证号码登录 +3. **身份证号码登录测试**: 模拟第三方跳转,验证Base64解码和登录流程 +4. **Tabbar导航测试**: 验证登录状态判断和页面跳转 + +## 文件清单 +- `pages/login/h5-login.vue` - H5账号密码登录页面 +- `pages/login/id-card-login.vue` - 身份证号码登录页面 +- `utils/loginHelper.js` - 登录帮助工具类 +- `apiRc/login.js` - 扩展的API接口 +- `components/wxAuthLogin/WxAuthLogin.vue` - 修复后的微信授权组件 +- `pages/index/components/index-one.vue` - 集成登录检查的首页组件 +- `components/CustomTabBar/CustomTabBar.vue` - 集成登录判断的Tabbar组件 +- `pages.json` - 页面路由配置 + +## 注意事项 +1. 身份证号码登录页面需要第三方系统通过URL参数传递`idCardBase64` +2. 后端需要实现`/app/idCardLogin`接口 +3. 各端Base64解码方式不同,需要确保兼容性 +4. 登录成功后需要正确存储用户信息和token + +系统已准备就绪,可以开始多端测试。