一体机登录功能开发

This commit is contained in:
冯辉
2025-11-14 18:10:59 +08:00
parent 9cda4eacb3
commit c798f1bdf2
14 changed files with 1317 additions and 146 deletions

View File

@@ -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() { export function getInfo() {
return request({ return request({
url: '/getInfo', url: '/getInfo',
method: 'get' method: 'get'
}) })
} }

View File

@@ -26,6 +26,7 @@ import { ref, computed, watch, onMounted } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore'; import useUserStore from '@/stores/useUserStore';
import { useReadMsg } from '@/stores/useReadMsg'; import { useReadMsg } from '@/stores/useReadMsg';
import { checkLoginAndNavigate } from '@/utils/loginHelper';
const props = defineProps({ const props = defineProps({
currentPage: { currentPage: {
@@ -157,77 +158,75 @@ watch(() => userInfo.value, (newUserInfo, oldUserInfo) => {
const switchTab = (item, index) => { const switchTab = (item, index) => {
console.log('switchTab called', item, index); console.log('switchTab called', item, index);
// 检查是否为"发布岗位"页面,需要判断企业信息是否完整 // 检查是否为需要登录的页面
if (item.path === '/pages/job/publishJob') { const loginRequiredPages = [
// 检查用户是否已登录 '/pages/job/publishJob',
const token = uni.getStorageSync('token') || ''; '/pages/mine/mine',
const hasLogin = userStore.hasLogin; '/pages/mine/company-mine'
];
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;
}
// 检查是否为"我的"页面,需要登录验证和用户类型判断 if (loginRequiredPages.includes(item.path)) {
if (item.path === '/pages/mine/mine') {
// 检查用户是否已登录 // 检查用户是否已登录
const token = uni.getStorageSync('token') || ''; const token = uni.getStorageSync('token') || '';
const hasLogin = userStore.hasLogin; const hasLogin = userStore.hasLogin;
if (!token || !hasLogin) { if (!token || !hasLogin) {
// 未登录,发送事件显示登录弹窗 // 未登录,根据平台类型跳转到对应的登录页面
uni.$emit('showLoginModal'); checkLoginAndNavigate();
return; // 不进行页面跳转 return; // 不进行页面跳转
} }
// 已登录,根据用户类型跳转到不同的"我的"页面 // 已登录,处理特定页面的逻辑
const cachedUserInfo = uni.getStorageSync('userInfo') || {}; if (item.path === '/pages/job/publishJob') {
const storeIsCompanyUser = userInfo.value?.isCompanyUser; // 检查企业信息是否完整
const cachedIsCompanyUser = cachedUserInfo.isCompanyUser; const cachedUserInfo = uni.getStorageSync('userInfo') || {};
const storeUserInfo = userInfo.value || {};
// 获取用户类型 const currentUserInfo = storeUserInfo.id ? storeUserInfo : cachedUserInfo;
const userType = Number(storeIsCompanyUser !== undefined ? storeIsCompanyUser : (cachedIsCompanyUser !== undefined ? cachedIsCompanyUser : 1));
// 判断企业信息字段company是否为null或undefined
let targetPath = '/pages/mine/mine'; // 默认求职者页面 if (!currentUserInfo.company || currentUserInfo.company === null) {
// 企业信息为空,跳转到企业信息补全页面
if (userType === 0) { uni.navigateTo({
// 企业用户,跳转到企业我的页面 url: '/pages/complete-info/company-info',
targetPath = '/pages/mine/company-mine'; });
} else { } else {
// 求职者或其他用户类型,跳转到普通我的页面 // 企业信息完整,跳转到发布岗位页面
targetPath = '/pages/mine/mine'; uni.navigateTo({
url: '/pages/job/publishJob',
});
}
currentItem.value = item.id;
return;
} }
// 跳转到对应的页面 if (item.path === '/pages/mine/mine') {
uni.navigateTo({ // 根据用户类型跳转到不同的"我的"页面
url: targetPath, const cachedUserInfo = uni.getStorageSync('userInfo') || {};
}); const storeIsCompanyUser = userInfo.value?.isCompanyUser;
const cachedIsCompanyUser = cachedUserInfo.isCompanyUser;
currentItem.value = item.id;
return; // 获取用户类型
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 页面 // 判断是否为 tabBar 页面

View File

@@ -9,7 +9,7 @@
<!-- Logo和标题 --> <!-- Logo和标题 -->
<view class="auth-header"> <view class="auth-header">
<image class="auth-logo" src="@/static/logo.png" mode="aspectFit"></image> <image class="auth-logo" src="@/static/logo2-S.png" mode="aspectFit"></image>
<view class="auth-title">欢迎使用就业服务</view> <view class="auth-title">欢迎使用就业服务</view>
<view class="auth-subtitle">需要您授权手机号登录</view> <view class="auth-subtitle">需要您授权手机号登录</view>
</view> </view>
@@ -227,65 +227,12 @@ const wxLogin = () => {
} }
// #ifdef H5 // #ifdef H5
// H5网页微信登录逻辑 // H5端跳转到H5登录页面
uni.showLoading({ title: '登录中...' }); close();
uni.navigateTo({
// 获取微信授权code url: '/pages/login/h5-login'
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('微信登录失败');
}
}); });
return;
// #endif // #endif
// #ifdef APP-PLUS // #ifdef APP-PLUS
@@ -561,4 +508,3 @@ defineExpose({
button::after button::after
border: none border: none
</style> </style>

View File

@@ -338,19 +338,19 @@
if (diffSeconds < 60) { if (diffSeconds < 60) {
return '刚刚发布'; return '刚刚发布';
} else if (diffMinutes < 60) { } else if (diffMinutes < 60) {
return `${diffMinutes}分钟前发布`; return `${diffMinutes}分钟前`;
} else if (diffHours < 24) { } else if (diffHours < 24) {
return `${diffHours}小时前发布`; return `${diffHours}小时前`;
} else if (diffDays === 1) { } else if (diffDays === 1) {
return '昨天发布'; return '昨天发布';
} else if (diffDays < 7) { } else if (diffDays < 7) {
return `${diffDays}天前发布`; return `${diffDays}天前`;
} else if (diffWeeks < 4) { } else if (diffWeeks < 4) {
return `${diffWeeks}周前发布`; return `${diffWeeks}周前`;
} else if (diffMonths < 12) { } else if (diffMonths < 12) {
return `${diffMonths}个月前发布`; return `${diffMonths}个月前`;
} else { } else {
return `${diffYears}年前发布`; return `${diffYears}年前`;
} }
} catch (error) { } catch (error) {
console.error('格式化发布时间失败:', error); console.error('格式化发布时间失败:', error);

View File

@@ -111,6 +111,20 @@
"style": { "style": {
"navigationBarTitleText": "编辑联系人" "navigationBarTitleText": "编辑联系人"
} }
},
{
"path": "pages/login/h5-login",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/login/id-card-login",
"style": {
"navigationBarTitleText": "社保登录",
"navigationStyle": "custom"
}
} }
], ],
"subpackages": [ "subpackages": [
@@ -658,4 +672,3 @@
}, },
"uniIdRouter": {} "uniIdRouter": {}
} }

View File

@@ -425,6 +425,7 @@ const { $api, navTo, vacanciesTo, formatTotal, config } = inject('globalFunction
import { onLoad, onShow } from '@dcloudio/uni-app'; import { onLoad, onShow } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore'; import useUserStore from '@/stores/useUserStore';
import { checkLoginAndNavigate, getPlatformType } from '@/utils/loginHelper';
const { userInfo, hasLogin, token } = storeToRefs(useUserStore()); const { userInfo, hasLogin, token } = storeToRefs(useUserStore());
// 计算是否显示求职者内容 // 计算是否显示求职者内容
@@ -685,8 +686,8 @@ watch([hasLogin, userInfo], () => {
const checkLogin = () => { const checkLogin = () => {
const tokenValue = uni.getStorageSync('token') || ''; const tokenValue = uni.getStorageSync('token') || '';
if (!tokenValue || !hasLogin.value) { if (!tokenValue || !hasLogin.value) {
// 未登录,打开授权弹窗 // 未登录,根据平台类型跳转到对应登录页面
wxAuthLoginRef.value?.open(); checkLoginAndNavigate();
return false; return false;
} }
return true; return true;

View File

@@ -118,7 +118,7 @@
v-model="formData.jobLocation" v-model="formData.jobLocation"
/> />
<view class="location-icon-btn" @click="chooseLocation"> <view class="location-icon-btn" @click="chooseLocation">
<uni-icons type="location" size="20" color="#256BFA"></uni-icons> <uni-icons type="location" size="20" color="#333"></uni-icons>
</view> </view>
</view> </view>
</view> </view>
@@ -800,25 +800,27 @@ const validateForm = () => {
.location-input { .location-input {
flex: 1; flex: 1;
padding-right: 80rpx; padding-right: 100rpx;
} }
.location-icon-btn { .location-icon-btn {
position: absolute; position: absolute;
right: -3px; right: 0;
top: 20%; top: 44%;
transform: translateY(-50%); transform: translateY(-50%);
width: 80rpx; width: 80rpx;
height: 80rpx; height: 60rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: transparent; background: #f0f0f0;
border: none; border: none;
border-radius: 8rpx; border-radius: 12rpx;
z-index: 99; transition: all 0.3s ease;
z-index: 99;
&:active { &:active {
background: #f0f0f0; transform: translateY(-50%) scale(0.95);
} }
} }
} }

529
pages/login/h5-login.vue Normal file
View File

@@ -0,0 +1,529 @@
<template>
<view class="login-container">
<!-- 背景装饰元素 -->
<view class="bg-decoration">
<view class="bg-circle bg-circle-1"></view>
<view class="bg-circle bg-circle-2"></view>
<view class="bg-circle bg-circle-3"></view>
</view>
<!-- Logo和标题 -->
<view class="login-header">
<view class="logo-wrapper">
<image class="logo" src="@/static/logo3.png" mode="aspectFit"></image>
</view>
<view class="welcome-section">
<view class="welcome-title">欢迎登录</view>
<view class="welcome-subtitle">社保就业服务平台</view>
</view>
</view>
<!-- 登录表单 -->
<view class="login-form">
<!-- 账号输入 -->
<view class="input-group">
<view class="input-label">账号</view>
<view class="input-item">
<uni-icons type="person-filled" size="28" color="#4778EC"></uni-icons>
<input
class="input"
placeholder="请输入手机号码"
placeholder-class="placeholder"
v-model="form.username"
type="text"
/>
</view>
</view>
<!-- 密码输入 -->
<view class="input-group">
<view class="input-label">密码</view>
<view class="input-item">
<uni-icons type="locked-filled" size="28" color="#4778EC"></uni-icons>
<input
class="input"
placeholder="请输入您的密码"
placeholder-class="placeholder"
v-model="form.password"
:type="showPassword ? 'text' : 'password'"
/>
<view class="password-toggle" @click="togglePasswordVisibility">
<uni-icons :type="showPassword ? 'eye-slash-filled' : 'eye-filled'" size="28" color="#999"></uni-icons>
</view>
</view>
<!-- 密码规则提示 -->
<view class="password-rules">
<view class="rule-item" :class="{ 'rule-valid': validateRule('length') }">
<uni-icons :type="validateRule('length') ? 'checkmarkempty' : 'circle'" size="20" :color="validateRule('length') ? '#52c41a' : '#999'"></uni-icons>
<text class="rule-text">长度8位</text>
</view>
<view class="rule-item" :class="{ 'rule-valid': validateRule('letterNumber') }">
<uni-icons :type="validateRule('letterNumber') ? 'checkmarkempty' : 'circle'" size="20" :color="validateRule('letterNumber') ? '#52c41a' : '#999'"></uni-icons>
<text class="rule-text">字母数字组合</text>
</view>
<view class="rule-item" :class="{ 'rule-valid': validateRule('upperLower') }">
<uni-icons :type="validateRule('upperLower') ? 'checkmarkempty' : 'circle'" size="20" :color="validateRule('upperLower') ? '#52c41a' : '#999'"></uni-icons>
<text class="rule-text">大小写字母</text>
</view>
</view>
</view>
<!-- 登录按钮 -->
<button class="login-btn" :class="{ 'login-btn-loading': loading }" @click="handleLogin">
<view class="btn-content">
<view class="btn-text" v-if="!loading">登录</view>
<view class="loading-spinner" v-else></view>
</view>
</button>
<!-- 其他登录方式 -->
<view class="other-login">
<view class="divider">
<view class="divider-line"></view>
<view class="divider-text">其他登录方式</view>
<view class="divider-line"></view>
</view>
<view class="login-options">
<view class="login-option" @click="goToIdCardLogin">
<view class="option-icon">
<uni-icons type="idcard" size="36" color="#4778EC"></uni-icons>
</view>
<view class="option-text">社保卡登录</view>
</view>
</view>
</view>
</view>
<!-- 底部信息 -->
<view class="footer">
<view class="footer-text">© 2024 社保就业服务平台</view>
</view>
</view>
</template>
<script setup>
import { reactive, inject, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import useUserStore from '@/stores/useUserStore'
const { $api } = inject("globalFunction")
const userStore = useUserStore()
const form = reactive({
username: '',
password: ''
})
const loading = ref(false)
const showPassword = ref(false)
// 切换密码可见性
const togglePasswordVisibility = () => {
showPassword.value = !showPassword.value
}
// 验证单个密码规则
const validateRule = (ruleType) => {
const password = form.password
if (!password) return false
switch (ruleType) {
case 'length':
return password.length === 8
case 'letterNumber':
return /[a-zA-Z]/.test(password) && /\d/.test(password)
case 'upperLower':
return /[A-Z]/.test(password) && /[a-z]/.test(password)
default:
return false
}
}
// 验证密码规则
const validatePassword = (password) => {
// 规则1: 必须有字母数字组合
const hasLetterAndNumber = /[a-zA-Z]/.test(password) && /\d/.test(password)
// 规则2: 至少一个大写和一个小写字母
const hasUpperCase = /[A-Z]/.test(password)
const hasLowerCase = /[a-z]/.test(password)
// 规则3: 长度必须是8位
const isLength8 = password.length === 8
if (!hasLetterAndNumber) {
return '密码必须包含字母和数字组合'
}
if (!hasUpperCase) {
return '密码必须包含至少一个大写字母'
}
if (!hasLowerCase) {
return '密码必须包含至少一个小写字母'
}
if (!isLength8) {
return '密码长度必须为8位'
}
return null // 验证通过
}
// 处理登录
const handleLogin = async () => {
if (!form.username) {
uni.showToast({
icon: 'none',
title: '请输入账号'
})
return
}
if (!form.password) {
uni.showToast({
icon: 'none',
title: '请输入密码'
})
return
}
// 验证密码规则
const passwordError = validatePassword(form.password)
if (passwordError) {
uni.showToast({
icon: 'none',
title: passwordError
})
return
}
loading.value = true
try {
// 调用账号密码登录接口
const res = await $api.createRequest('/app/login', {
username: form.username,
password: form.password
}, 'post')
if (res.token) {
// 登录成功存储token并获取用户信息
await userStore.loginSetToken(res.token)
uni.showToast({
title: '登录成功',
icon: 'success'
})
// 跳转到首页
uni.reLaunch({
url: '/pages/index/index'
})
} else {
uni.showToast({
title: '登录失败',
icon: 'none'
})
}
} catch (error) {
console.error('登录失败:', error)
uni.showToast({
title: error.msg || '登录失败,请重试',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 跳转到身份证号码登录页面
const goToIdCardLogin = () => {
uni.navigateTo({
url: '/pages/login/id-card-login'
})
}
onLoad(() => {
// 页面加载时的初始化逻辑
})
</script>
<style lang="stylus" scoped>
.login-container
padding: 0 40rpx
min-height: 100vh
background: linear-gradient(135deg, #4778EC 0%, #256BFA 100%)
position: relative
overflow: hidden
.bg-decoration
position: absolute
top: 0
left: 0
width: 100%
height: 100%
pointer-events: none
z-index: 1
.bg-circle
position: absolute
border-radius: 50%
background: rgba(255, 255, 255, 0.1)
.bg-circle-1
width: 400rpx
height: 400rpx
top: -200rpx
right: -100rpx
.bg-circle-2
width: 300rpx
height: 300rpx
bottom: 100rpx
left: -150rpx
.bg-circle-3
width: 200rpx
height: 200rpx
bottom: -50rpx
right: 100rpx
.login-header
position: relative
z-index: 2
text-align: center
margin-bottom: 80rpx
padding-top: 120rpx
.logo-wrapper
margin-bottom: 40rpx
.logo
width: 441rpx
height: 160rpx
filter: drop-shadow(0 8rpx 20rpx rgba(0, 0, 0, 0.2))
.welcome-section
.welcome-title
font-size: 48rpx
font-weight: 700
color: #FFFFFF
margin-bottom: 16rpx
text-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.2)
.welcome-subtitle
font-size: 32rpx
color: rgba(255, 255, 255, 0.9)
font-weight: 500
.login-form
position: relative
z-index: 2
background: #FFFFFF
border-radius: 32rpx
padding: 60rpx 40rpx
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.15)
backdrop-filter: blur(20rpx)
border: 1rpx solid rgba(255, 255, 255, 0.2)
.input-group
margin-bottom: 48rpx
.input-label
font-size: 28rpx
font-weight: 600
color: #333333
margin-bottom: 20rpx
padding-left: 10rpx
.input-item
display: flex
align-items: center
padding: 0 32rpx
height: 100rpx
background: #F8F9FF
border-radius: 25rpx
border: 2rpx solid #E8ECFF
transition: all 0.3s ease
&:focus-within
border-color: #4778EC
background: #FFFFFF
box-shadow: 0 8rpx 24rpx rgba(71, 120, 236, 0.15)
transform: translateY(-2rpx)
.input
flex: 1
height: 100%
margin-left: 24rpx
font-size: 32rpx
color: #333333
background: transparent
font-weight: 500
.password-toggle
display: flex
align-items: center
justify-content: center
width: 60rpx
height: 60rpx
border-radius: 50%
transition: all 0.3s ease
cursor: pointer
&:active
background: rgba(71, 120, 236, 0.1)
transform: scale(0.9)
.placeholder
font-size: 32rpx
color: #999999
font-weight: 400
.password-rules
margin-top: 20rpx
display: flex
flex-wrap: wrap
gap: 20rpx
.rule-item
display: flex
align-items: center
font-size: 24rpx
color: #999999
transition: all 0.3s ease
&.rule-valid
color: #52c41a
.rule-text
margin-left: 8rpx
font-size: 24rpx
.login-btn
width: 100%
height: 100rpx
background: linear-gradient(135deg, #4778EC 0%, #256BFA 100%)
border-radius: 25rpx
color: #FFFFFF
font-size: 34rpx
font-weight: 600
border: none
margin-bottom: 48rpx
box-shadow: 0 12rpx 30rpx rgba(37, 107, 250, 0.4)
transition: all 0.3s ease
position: relative
overflow: hidden
&:active
transform: translateY(2rpx)
box-shadow: 0 6rpx 20rpx rgba(37, 107, 250, 0.3)
&.login-btn-loading
background: linear-gradient(135deg, #4778EC 0%, #256BFA 100%)
opacity: 0.8
.btn-content
display: flex
align-items: center
justify-content: center
height: 100%
.btn-text
font-size: 34rpx
font-weight: 600
.loading-spinner
width: 40rpx
height: 40rpx
border: 4rpx solid rgba(255, 255, 255, 0.3)
border-radius: 50%
border-top: 4rpx solid #FFFFFF
animation: spin 1s linear infinite
.other-login
.divider
display: flex
align-items: center
margin-bottom: 40rpx
.divider-line
flex: 1
height: 1rpx
background: #E5E5E5
.divider-text
padding: 0 24rpx
font-size: 26rpx
color: #999999
font-weight: 500
.login-options
display: flex
justify-content: center
.login-option
display: flex
flex-direction: column
align-items: center
padding: 20rpx 40rpx
border-radius: 20rpx
background: #F8F9FF
border: 2rpx solid #E8ECFF
transition: all 0.3s ease
cursor: pointer
&:active
background: #F0F4FF
transform: scale(0.95)
.option-icon
margin-bottom: 16rpx
.option-text
font-size: 24rpx
color: #4778EC
font-weight: 500
.footer
position: relative
z-index: 2
text-align: center
margin-top: 60rpx
padding-bottom: 40rpx
.footer-text
font-size: 24rpx
color: rgba(255, 255, 255, 0.7)
// 按钮重置样式
button::after
border: none
// 动画定义
@keyframes spin
0%
transform: rotate(0deg)
100%
transform: rotate(360deg)
// 响应式设计
@media (max-width: 750px)
.login-container
padding: 0 32rpx
.login-header
padding-top: 80rpx
margin-bottom: 60rpx
.logo
width: 360rpx
height: 130rpx
.welcome-title
font-size: 40rpx
.welcome-subtitle
font-size: 28rpx
.login-form
padding: 48rpx 32rpx
border-radius: 24rpx
</style>

View File

@@ -0,0 +1,372 @@
<template>
<view class="loading-container">
<!-- 背景装饰元素 -->
<view class="bg-decoration">
<view class="bg-circle bg-circle-1"></view>
<view class="bg-circle bg-circle-2"></view>
<view class="bg-circle bg-circle-3"></view>
</view>
<!-- Loading内容 -->
<view class="loading-content">
<!-- 图标区域 -->
<view class="icon-section">
<view class="icon-wrapper">
<uni-icons type="idcard-filled" size="80" color="#FFFFFF"></uni-icons>
</view>
</view>
<!-- Loading动画 -->
<view class="loading-animation">
<view class="spinner"></view>
</view>
<!-- 文字内容 -->
<view class="text-section">
<view class="loading-text">登录中请稍候...</view>
<view class="loading-subtext">正在验证您的社保卡信息</view>
</view>
<!-- 进度指示器 -->
<view class="progress-section">
<view class="progress-bar">
<view class="progress-fill"></view>
</view>
<view class="progress-text">验证中...</view>
</view>
</view>
<!-- 底部信息 -->
<view class="footer">
<view class="footer-text">© 2024 社保就业服务平台</view>
</view>
</view>
</template>
<script setup>
import { inject, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import useUserStore from '@/stores/useUserStore'
const { $api } = inject("globalFunction")
const userStore = useUserStore()
// 处理身份证号码登录
const handleIdCardLogin = async (idCard) => {
try {
// 调用身份证号码登录接口
const res = await $api.createRequest('/app/idCardLogin', {
idCard: idCard
}, 'post')
if (res.token) {
// 登录成功存储token并获取用户信息
await userStore.loginSetToken(res.token)
// 获取用户详细信息
await userStore.getUserResume()
uni.showToast({
title: '登录成功',
icon: 'success'
})
// 跳转到首页
uni.reLaunch({
url: '/pages/index/index'
})
} else {
uni.showToast({
title: '登录失败',
icon: 'none'
})
// 登录失败,返回登录页面
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
} catch (error) {
console.error('身份证登录失败:', error)
uni.showToast({
title: error.msg || '登录失败,请重试',
icon: 'none'
})
// 登录失败,返回登录页面
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
}
// Base64解码
const decodeBase64 = (base64Str) => {
try {
// 在H5环境中使用atob解码
// #ifdef H5
return atob(base64Str)
// #endif
// 在小程序环境中使用uni.base64ToArrayBuffer
// #ifdef MP-WEIXIN
const arrayBuffer = uni.base64ToArrayBuffer(base64Str)
const uint8Array = new Uint8Array(arrayBuffer)
let result = ''
for (let i = 0; i < uint8Array.length; i++) {
result += String.fromCharCode(uint8Array[i])
}
return result
// #endif
// 在App环境中
// #ifdef APP-PLUS
return plus.base64.decode(base64Str)
// #endif
} catch (error) {
console.error('Base64解码失败:', error)
throw new Error('身份证号码解码失败')
}
}
// 解析URL参数
const getUrlParams = (url) => {
const params = {}
const urlObj = new URL(url)
const searchParams = new URLSearchParams(urlObj.search)
for (const [key, value] of searchParams) {
params[key] = value
}
return params
}
onLoad((options) => {
// 处理第三方跳转过来的身份证登录
if (options.idCardBase64) {
// 直接通过参数传递
processIdCardLogin(options.idCardBase64)
} else {
// 从当前URL中获取参数
const currentPages = getCurrentPages()
const currentPage = currentPages[currentPages.length - 1]
const url = currentPage.$page.fullPath
const params = getUrlParams(url)
if (params.idCardBase64) {
processIdCardLogin(params.idCardBase64)
} else {
uni.showToast({
title: '缺少身份证号码参数',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
}
})
// 处理身份证登录流程
const processIdCardLogin = async (idCardBase64) => {
try {
// 解码Base64身份证号码
const idCard = decodeBase64(idCardBase64)
// 验证身份证号码格式(简单验证)
if (!idCard || idCard.length < 15) {
throw new Error('身份证号码格式不正确')
}
// 调用登录接口
await handleIdCardLogin(idCard)
} catch (error) {
console.error('处理身份证登录失败:', error)
uni.showToast({
title: error.message || '处理身份证信息失败',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
}
onMounted(() => {
// 页面挂载后的逻辑
})
</script>
<style lang="stylus" scoped>
.loading-container
padding: 0 40rpx
min-height: 100vh
background: linear-gradient(135deg, #4778EC 0%, #256BFA 100%)
position: relative
overflow: hidden
display: flex
flex-direction: column
justify-content: center
align-items: center
.bg-decoration
position: absolute
top: 0
left: 0
width: 100%
height: 100%
pointer-events: none
z-index: 1
.bg-circle
position: absolute
border-radius: 50%
background: rgba(255, 255, 255, 0.1)
.bg-circle-1
width: 400rpx
height: 400rpx
top: -200rpx
right: -100rpx
.bg-circle-2
width: 300rpx
height: 300rpx
bottom: 100rpx
left: -150rpx
.bg-circle-3
width: 200rpx
height: 200rpx
bottom: -50rpx
right: 100rpx
.loading-content
position: relative
z-index: 2
text-align: center
color: #FFFFFF
width: 100%
max-width: 600rpx
.icon-section
margin-bottom: 60rpx
.icon-wrapper
display: inline-flex
align-items: center
justify-content: center
width: 160rpx
height: 160rpx
background: rgba(255, 255, 255, 0.2)
border-radius: 50%
backdrop-filter: blur(10rpx)
border: 2rpx solid rgba(255, 255, 255, 0.3)
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2)
.loading-animation
margin-bottom: 60rpx
.spinner
width: 80rpx
height: 80rpx
margin: 0 auto
border: 6rpx solid rgba(255, 255, 255, 0.3)
border-radius: 50%
border-top: 6rpx solid #FFFFFF
animation: spin 1s linear infinite
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.2)
.text-section
margin-bottom: 60rpx
.loading-text
font-size: 40rpx
font-weight: 700
margin-bottom: 20rpx
text-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.2)
.loading-subtext
font-size: 30rpx
opacity: 0.9
font-weight: 500
.progress-section
.progress-bar
width: 100%
height: 8rpx
background: rgba(255, 255, 255, 0.3)
border-radius: 4rpx
overflow: hidden
margin-bottom: 20rpx
.progress-fill
width: 60%
height: 100%
background: linear-gradient(90deg, #FFFFFF 0%, rgba(255, 255, 255, 0.8) 100%)
border-radius: 4rpx
animation: progress 2s ease-in-out infinite alternate
.progress-text
font-size: 26rpx
opacity: 0.8
font-weight: 500
.footer
position: absolute
bottom: 40rpx
left: 0
right: 0
text-align: center
z-index: 2
.footer-text
font-size: 24rpx
color: rgba(255, 255, 255, 0.7)
// 动画定义
@keyframes spin
0%
transform: rotate(0deg)
100%
transform: rotate(360deg)
@keyframes progress
0%
width: 30%
transform: translateX(-100%)
50%
width: 60%
100%
width: 30%
transform: translateX(233%)
// 响应式设计
@media (max-width: 750px)
.loading-container
padding: 0 32rpx
.loading-content
max-width: 500rpx
.icon-section
margin-bottom: 40rpx
.icon-wrapper
width: 120rpx
height: 120rpx
.loading-animation
margin-bottom: 40rpx
.text-section
margin-bottom: 40rpx
.loading-text
font-size: 36rpx
.loading-subtext
font-size: 28rpx
.progress-section
.progress-text
font-size: 24rpx
</style>

BIN
static/logo1-S.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
static/logo2-S.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/logo3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

158
utils/loginHelper.js Normal file
View File

@@ -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
};

139
登录系统实现总结.md Normal file
View File

@@ -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
系统已准备就绪,可以开始多端测试。