微信授权登录功能开发联调
This commit is contained in:
22
App.vue
22
App.vue
@@ -11,24 +11,26 @@ onLaunch((options) => {
|
|||||||
useDictStore().getDictData();
|
useDictStore().getDictData();
|
||||||
// uni.hideTabBar();
|
// uni.hideTabBar();
|
||||||
|
|
||||||
// 登录
|
// 尝试从缓存恢复用户信息
|
||||||
let token = uni.getStorageSync('token') || ''; // 同步获取 缓存信息
|
const restored = useUserStore().restoreUserInfo();
|
||||||
|
|
||||||
|
if (restored) {
|
||||||
|
// 如果成功恢复用户信息,验证token是否有效
|
||||||
|
let token = uni.getStorageSync('token') || '';
|
||||||
if (token) {
|
if (token) {
|
||||||
useUserStore()
|
useUserStore()
|
||||||
.loginSetToken(token)
|
.loginSetToken(token)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
$api.msg('登录成功');
|
console.log('用户登录状态已恢复');
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
uni.redirectTo({
|
// token无效,清除缓存(不跳转登录页)
|
||||||
url: '/pages/login/login',
|
console.log('token已过期,需要重新登录');
|
||||||
|
useUserStore().logOut(false);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// uni.redirectTo({
|
|
||||||
// url: '/pages/login/login',
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// 不再强制跳转到登录页,而是在需要登录时弹出授权弹窗
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
386
components/wxAuthLogin/WxAuthLogin.vue
Normal file
386
components/wxAuthLogin/WxAuthLogin.vue
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
<template>
|
||||||
|
<uni-popup ref="popup" type="center" :mask-click="false">
|
||||||
|
<view class="auth-modal">
|
||||||
|
<view class="modal-content">
|
||||||
|
<!-- 关闭按钮 -->
|
||||||
|
<view class="close-btn" @click="close">
|
||||||
|
<uni-icons type="closeempty" size="24" color="#999"></uni-icons>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Logo和标题 -->
|
||||||
|
<view class="auth-header">
|
||||||
|
<image class="auth-logo" src="@/static/logo.png" mode="aspectFit"></image>
|
||||||
|
<view class="auth-title">欢迎使用就业服务</view>
|
||||||
|
<view class="auth-subtitle">需要您授权手机号登录</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 授权说明 -->
|
||||||
|
<view class="auth-tips">
|
||||||
|
<view class="tip-item">
|
||||||
|
<uni-icons type="checkmarkempty" size="16" color="#13C57C"></uni-icons>
|
||||||
|
<text>保护您的个人信息安全</text>
|
||||||
|
</view>
|
||||||
|
<view class="tip-item">
|
||||||
|
<uni-icons type="checkmarkempty" size="16" color="#13C57C"></uni-icons>
|
||||||
|
<text>为您推荐更合适的岗位</text>
|
||||||
|
</view>
|
||||||
|
<view class="tip-item">
|
||||||
|
<uni-icons type="checkmarkempty" size="16" color="#13C57C"></uni-icons>
|
||||||
|
<text>享受完整的就业服务</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 授权按钮 -->
|
||||||
|
<view class="auth-actions">
|
||||||
|
<!-- 微信小程序使用 open-type="getPhoneNumber" -->
|
||||||
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
<button
|
||||||
|
class="auth-btn primary"
|
||||||
|
open-type="getPhoneNumber"
|
||||||
|
@getphonenumber="getPhoneNumber"
|
||||||
|
>
|
||||||
|
<uni-icons type="phone" size="20" color="#FFFFFF"></uni-icons>
|
||||||
|
<text>微信授权登录</text>
|
||||||
|
</button>
|
||||||
|
<!-- #endif -->
|
||||||
|
|
||||||
|
<!-- H5和App使用普通按钮 -->
|
||||||
|
<!-- #ifndef MP-WEIXIN -->
|
||||||
|
<button class="auth-btn primary" @click="wxLogin">
|
||||||
|
<uni-icons type="phone" size="20" color="#FFFFFF"></uni-icons>
|
||||||
|
<text>微信授权登录</text>
|
||||||
|
</button>
|
||||||
|
<!-- #endif -->
|
||||||
|
|
||||||
|
<!-- 测试登录按钮(仅开发环境) -->
|
||||||
|
<!-- #ifdef APP-PLUS || H5 -->
|
||||||
|
<button class="auth-btn secondary" @click="testLogin">
|
||||||
|
<text>测试账号登录</text>
|
||||||
|
</button>
|
||||||
|
<!-- #endif -->
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 用户协议 -->
|
||||||
|
<view class="auth-agreement">
|
||||||
|
<text>登录即表示同意</text>
|
||||||
|
<text class="link" @click="openAgreement('user')">《用户协议》</text>
|
||||||
|
<text>和</text>
|
||||||
|
<text class="link" @click="openAgreement('privacy')">《隐私政策》</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, inject } from 'vue';
|
||||||
|
import useUserStore from '@/stores/useUserStore';
|
||||||
|
|
||||||
|
const { $api } = inject('globalFunction');
|
||||||
|
const { loginSetToken } = useUserStore();
|
||||||
|
|
||||||
|
const popup = ref(null);
|
||||||
|
const emit = defineEmits(['success', 'cancel']);
|
||||||
|
|
||||||
|
// 打开弹窗
|
||||||
|
const open = () => {
|
||||||
|
popup.value?.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
const close = () => {
|
||||||
|
popup.value?.close();
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 微信小程序获取手机号
|
||||||
|
const getPhoneNumber = (e) => {
|
||||||
|
console.log('获取手机号:', e);
|
||||||
|
|
||||||
|
if (e.detail.errMsg === 'getPhoneNumber:ok') {
|
||||||
|
const { code, encryptedData, iv } = e.detail;
|
||||||
|
|
||||||
|
// 调用后端接口进行登录
|
||||||
|
uni.showLoading({ title: '登录中...' });
|
||||||
|
|
||||||
|
$api.createRequest('/app/appLogin', {
|
||||||
|
code,
|
||||||
|
encryptedData,
|
||||||
|
iv
|
||||||
|
}, 'post').then((resData) => {
|
||||||
|
uni.hideLoading();
|
||||||
|
|
||||||
|
if (resData.token) {
|
||||||
|
// 登录成功,存储token
|
||||||
|
loginSetToken(resData.token).then((resume) => {
|
||||||
|
$api.msg('登录成功');
|
||||||
|
close();
|
||||||
|
emit('success');
|
||||||
|
|
||||||
|
// 如果用户信息不完整,跳转到完善信息页面
|
||||||
|
if (!resume.data.jobTitleId) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/login/login?step=1'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
$api.msg('获取用户信息失败');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$api.msg('登录失败,请重试');
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
uni.hideLoading();
|
||||||
|
$api.msg(err.msg || '登录失败,请重试');
|
||||||
|
});
|
||||||
|
} else if (e.detail.errMsg === 'getPhoneNumber:fail user deny') {
|
||||||
|
$api.msg('您取消了授权');
|
||||||
|
} else {
|
||||||
|
$api.msg('获取手机号失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// H5/App 微信登录
|
||||||
|
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
|
||||||
|
}, 'post').then((resData) => {
|
||||||
|
uni.hideLoading();
|
||||||
|
|
||||||
|
if (resData.token) {
|
||||||
|
loginSetToken(resData.token).then((resume) => {
|
||||||
|
$api.msg('登录成功');
|
||||||
|
close();
|
||||||
|
emit('success');
|
||||||
|
|
||||||
|
if (!resume.data.jobTitleId) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/login/login?step=1'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$api.msg('登录失败,请重试');
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
uni.hideLoading();
|
||||||
|
$api.msg(err.msg || '登录失败,请重试');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
uni.hideLoading();
|
||||||
|
console.error('微信登录失败:', err);
|
||||||
|
$api.msg('微信登录失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
// App微信登录逻辑
|
||||||
|
uni.getProvider({
|
||||||
|
service: 'oauth',
|
||||||
|
success: (res) => {
|
||||||
|
if (~res.provider.indexOf('weixin')) {
|
||||||
|
uni.login({
|
||||||
|
provider: 'weixin',
|
||||||
|
success: (loginRes) => {
|
||||||
|
console.log('微信登录成功:', loginRes);
|
||||||
|
|
||||||
|
// 调用后端接口进行登录
|
||||||
|
$api.createRequest('/app/appLogin', {
|
||||||
|
code: loginRes.code
|
||||||
|
}, 'post').then((resData) => {
|
||||||
|
if (resData.token) {
|
||||||
|
loginSetToken(resData.token).then((resume) => {
|
||||||
|
$api.msg('登录成功');
|
||||||
|
close();
|
||||||
|
emit('success');
|
||||||
|
|
||||||
|
if (!resume.data.jobTitleId) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/login/login?step=1'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('微信登录失败:', err);
|
||||||
|
$api.msg('微信登录失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
};
|
||||||
|
|
||||||
|
// 测试账号登录(仅开发环境)
|
||||||
|
const testLogin = () => {
|
||||||
|
uni.showLoading({ title: '登录中...' });
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
username: 'test',
|
||||||
|
password: 'test',
|
||||||
|
};
|
||||||
|
|
||||||
|
$api.createRequest('/app/login', params, 'post').then((resData) => {
|
||||||
|
uni.hideLoading();
|
||||||
|
|
||||||
|
loginSetToken(resData.token).then((resume) => {
|
||||||
|
$api.msg('测试登录成功');
|
||||||
|
close();
|
||||||
|
emit('success');
|
||||||
|
|
||||||
|
if (!resume.data.jobTitleId) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/login/login?step=1'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
$api.msg('获取用户信息失败');
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
uni.hideLoading();
|
||||||
|
$api.msg(err.msg || '登录失败');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开用户协议
|
||||||
|
const openAgreement = (type) => {
|
||||||
|
const urls = {
|
||||||
|
user: '/pages/agreement/user',
|
||||||
|
privacy: '/pages/agreement/privacy'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (urls[type]) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: urls[type]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 暴露方法供父组件调用
|
||||||
|
defineExpose({
|
||||||
|
open,
|
||||||
|
close
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.auth-modal
|
||||||
|
width: 620rpx
|
||||||
|
background: #FFFFFF
|
||||||
|
border-radius: 24rpx
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
.modal-content
|
||||||
|
padding: 60rpx 40rpx 40rpx
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
.close-btn
|
||||||
|
position: absolute
|
||||||
|
right: 20rpx
|
||||||
|
top: 20rpx
|
||||||
|
width: 60rpx
|
||||||
|
height: 60rpx
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
z-index: 10
|
||||||
|
|
||||||
|
.auth-header
|
||||||
|
text-align: center
|
||||||
|
margin-bottom: 40rpx
|
||||||
|
|
||||||
|
.auth-logo
|
||||||
|
width: 120rpx
|
||||||
|
height: 120rpx
|
||||||
|
margin: 0 auto 24rpx
|
||||||
|
|
||||||
|
.auth-title
|
||||||
|
font-size: 36rpx
|
||||||
|
font-weight: 600
|
||||||
|
color: #333333
|
||||||
|
margin-bottom: 12rpx
|
||||||
|
|
||||||
|
.auth-subtitle
|
||||||
|
font-size: 28rpx
|
||||||
|
color: #666666
|
||||||
|
|
||||||
|
.auth-tips
|
||||||
|
background: #F7F8FA
|
||||||
|
border-radius: 16rpx
|
||||||
|
padding: 24rpx
|
||||||
|
margin-bottom: 40rpx
|
||||||
|
|
||||||
|
.tip-item
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
margin-bottom: 16rpx
|
||||||
|
font-size: 26rpx
|
||||||
|
color: #666666
|
||||||
|
|
||||||
|
&:last-child
|
||||||
|
margin-bottom: 0
|
||||||
|
|
||||||
|
text
|
||||||
|
margin-left: 12rpx
|
||||||
|
|
||||||
|
.auth-actions
|
||||||
|
margin-bottom: 32rpx
|
||||||
|
|
||||||
|
.auth-btn
|
||||||
|
width: 100%
|
||||||
|
height: 88rpx
|
||||||
|
border-radius: 44rpx
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
font-size: 32rpx
|
||||||
|
font-weight: 500
|
||||||
|
border: none
|
||||||
|
margin-bottom: 20rpx
|
||||||
|
|
||||||
|
&:last-child
|
||||||
|
margin-bottom: 0
|
||||||
|
|
||||||
|
&.primary
|
||||||
|
background: linear-gradient(135deg, #13C57C 0%, #0FA368 100%)
|
||||||
|
color: #FFFFFF
|
||||||
|
box-shadow: 0 8rpx 20rpx rgba(19, 197, 124, 0.3)
|
||||||
|
|
||||||
|
&.secondary
|
||||||
|
background: #F7F8FA
|
||||||
|
color: #666666
|
||||||
|
|
||||||
|
text
|
||||||
|
margin-left: 12rpx
|
||||||
|
|
||||||
|
.auth-agreement
|
||||||
|
text-align: center
|
||||||
|
font-size: 24rpx
|
||||||
|
color: #999999
|
||||||
|
line-height: 1.6
|
||||||
|
|
||||||
|
.link
|
||||||
|
color: #256BFA
|
||||||
|
text-decoration: underline
|
||||||
|
|
||||||
|
// 按钮重置样式
|
||||||
|
button::after
|
||||||
|
border: none
|
||||||
|
</style>
|
||||||
|
|
321
docs/微信授权登录功能说明.md
Normal file
321
docs/微信授权登录功能说明.md
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
# 微信授权登录功能说明
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
本次开发实现了微信授权登录功能,当用户点击首页的特定功能时,会检查用户是否已登录,如果未登录则弹出授权弹窗,而不是直接跳转到登录页面。
|
||||||
|
|
||||||
|
## 主要功能点
|
||||||
|
|
||||||
|
### 1. 需要登录验证的功能入口
|
||||||
|
|
||||||
|
以下功能在点击时会进行登录验证:
|
||||||
|
|
||||||
|
- **附近工作** - 点击后跳转到附近工作列表页
|
||||||
|
- **九宫格服务功能** - 包含9个服务项:
|
||||||
|
- 服务指导
|
||||||
|
- 事业单位招录
|
||||||
|
- 简历制作
|
||||||
|
- 劳动政策指引
|
||||||
|
- 技能培训信息
|
||||||
|
- 技能评价指引
|
||||||
|
- 题库和考试
|
||||||
|
- 素质测评
|
||||||
|
- AI智能面试
|
||||||
|
- **职位列表** - 点击任意职位卡片查看详情
|
||||||
|
|
||||||
|
### 2. 登录弹窗功能
|
||||||
|
|
||||||
|
#### 弹窗特性
|
||||||
|
- 使用 `uni-popup` 组件实现弹窗效果
|
||||||
|
- 弹窗居中显示,支持关闭按钮
|
||||||
|
- 不可点击遮罩关闭,确保用户必须做出选择
|
||||||
|
|
||||||
|
#### 弹窗内容
|
||||||
|
- **Logo和标题** - 显示应用logo和欢迎信息
|
||||||
|
- **授权说明** - 列出三个要点:
|
||||||
|
- 保护您的个人信息安全
|
||||||
|
- 为您推荐更合适的岗位
|
||||||
|
- 享受完整的就业服务
|
||||||
|
- **授权按钮**:
|
||||||
|
- 微信小程序:使用 `open-type="getPhoneNumber"` 获取手机号
|
||||||
|
- H5/App:使用微信登录接口
|
||||||
|
- 测试登录按钮(仅H5/App环境显示)
|
||||||
|
- **用户协议** - 显示用户协议和隐私政策链接
|
||||||
|
|
||||||
|
### 3. 登录流程
|
||||||
|
|
||||||
|
#### 微信小程序登录流程
|
||||||
|
1. 用户点击"微信授权登录"按钮
|
||||||
|
2. 触发微信小程序的手机号授权
|
||||||
|
3. 获取到 `code`、`encryptedData`、`iv`
|
||||||
|
4. 调用后端 `/app/wxLogin` 接口
|
||||||
|
5. 后端返回 `token`
|
||||||
|
6. 存储 `token` 并获取用户信息
|
||||||
|
7. 如果用户信息不完整,跳转到完善信息页面
|
||||||
|
8. 关闭弹窗,继续用户之前的操作
|
||||||
|
|
||||||
|
#### H5/App登录流程
|
||||||
|
1. 用户点击"微信授权登录"按钮
|
||||||
|
2. 调用 `uni.login` 获取微信授权 `code`
|
||||||
|
3. 调用后端 `/app/wxLogin` 接口
|
||||||
|
4. 后续流程同上
|
||||||
|
|
||||||
|
#### 测试登录流程(仅开发环境)
|
||||||
|
1. 用户点击"测试账号登录"按钮
|
||||||
|
2. 使用测试账号密码登录
|
||||||
|
3. 后续流程同上
|
||||||
|
|
||||||
|
### 4. 登录状态管理
|
||||||
|
|
||||||
|
#### 状态恢复
|
||||||
|
- 应用启动时自动从本地缓存恢复用户信息
|
||||||
|
- 验证 `token` 是否有效
|
||||||
|
- 如果 `token` 失效,清除缓存但不跳转登录页
|
||||||
|
|
||||||
|
#### 状态检查
|
||||||
|
- 使用 `checkLogin()` 函数统一检查登录状态
|
||||||
|
- 检查 `token` 是否存在
|
||||||
|
- 检查 `hasLogin` 状态
|
||||||
|
- 如果未登录,自动打开授权弹窗
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
ks-app-employment-service/
|
||||||
|
├── components/
|
||||||
|
│ └── WxAuthLogin/
|
||||||
|
│ └── WxAuthLogin.vue # 微信授权登录弹窗组件
|
||||||
|
├── pages/
|
||||||
|
│ └── index/
|
||||||
|
│ └── components/
|
||||||
|
│ └── index-one.vue # 首页组件(已修改)
|
||||||
|
├── stores/
|
||||||
|
│ └── useUserStore.js # 用户状态管理(已修改)
|
||||||
|
├── App.vue # 应用入口(已修改)
|
||||||
|
└── docs/
|
||||||
|
└── 微信授权登录功能说明.md # 本文档
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心代码说明
|
||||||
|
|
||||||
|
### 1. WxAuthLogin.vue 组件
|
||||||
|
|
||||||
|
这是一个可复用的微信授权登录弹窗组件,提供以下接口:
|
||||||
|
|
||||||
|
**Props**
|
||||||
|
- 无
|
||||||
|
|
||||||
|
**Events**
|
||||||
|
- `success` - 登录成功时触发
|
||||||
|
- `cancel` - 取消登录时触发
|
||||||
|
|
||||||
|
**Methods**
|
||||||
|
- `open()` - 打开弹窗
|
||||||
|
- `close()` - 关闭弹窗
|
||||||
|
|
||||||
|
**使用示例**
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<WxAuthLogin ref="wxAuthLoginRef" @success="handleLoginSuccess" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import WxAuthLogin from '@/components/WxAuthLogin/WxAuthLogin.vue';
|
||||||
|
|
||||||
|
const wxAuthLoginRef = ref(null);
|
||||||
|
|
||||||
|
const handleLoginSuccess = () => {
|
||||||
|
console.log('登录成功');
|
||||||
|
// 执行登录后的操作
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开登录弹窗
|
||||||
|
const showLogin = () => {
|
||||||
|
wxAuthLoginRef.value?.open();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 登录检查函数
|
||||||
|
|
||||||
|
在 `index-one.vue` 中添加了统一的登录检查函数:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 登录检查函数
|
||||||
|
const checkLogin = () => {
|
||||||
|
const tokenValue = uni.getStorageSync('token') || '';
|
||||||
|
if (!tokenValue || !hasLogin.value) {
|
||||||
|
// 未登录,打开授权弹窗
|
||||||
|
wxAuthLoginRef.value?.open();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 点击事件处理
|
||||||
|
|
||||||
|
所有需要登录的功能都使用统一的检查逻辑:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 处理附近工作点击
|
||||||
|
const handleNearbyClick = () => {
|
||||||
|
if (checkLogin()) {
|
||||||
|
navTo('/pages/nearby/nearby');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理服务功能点击
|
||||||
|
const handleServiceClick = (serviceType) => {
|
||||||
|
if (checkLogin()) {
|
||||||
|
navToService(serviceType);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理职位详情点击
|
||||||
|
function nextDetail(job) {
|
||||||
|
if (checkLogin()) {
|
||||||
|
// 记录岗位类型,用作数据分析
|
||||||
|
if (job.jobCategory) {
|
||||||
|
const recordData = recommedIndexDb.JobParameter(job);
|
||||||
|
recommedIndexDb.addRecord(recordData);
|
||||||
|
}
|
||||||
|
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 状态管理优化
|
||||||
|
|
||||||
|
在 `useUserStore.js` 中优化了 `logOut` 函数:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const logOut = (redirect = true) => {
|
||||||
|
hasLogin.value = false;
|
||||||
|
token.value = ''
|
||||||
|
resume.value = {}
|
||||||
|
userInfo.value = {}
|
||||||
|
role.value = {}
|
||||||
|
uni.removeStorageSync('userInfo')
|
||||||
|
uni.removeStorageSync('token')
|
||||||
|
|
||||||
|
// 只有在明确需要跳转时才跳转到登录页
|
||||||
|
if (redirect) {
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages/login/login',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 后端接口要求
|
||||||
|
|
||||||
|
### 1. 微信登录接口
|
||||||
|
|
||||||
|
**接口地址**: `/app/appLogin`
|
||||||
|
**请求方法**: `POST`
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
#### 微信小程序
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "string", // 微信登录凭证
|
||||||
|
"encryptedData": "string", // 加密数据
|
||||||
|
"iv": "string" // 加密算法初始向量
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### H5/App
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "string" // 微信登录凭证
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回数据**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "string", // 用户token
|
||||||
|
"msg": "string", // 返回消息
|
||||||
|
"code": 200 // 状态码
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 获取用户信息接口
|
||||||
|
|
||||||
|
**接口地址**: `/app/user/resume`
|
||||||
|
**请求方法**: `GET`
|
||||||
|
**请求头**: `Authorization: Bearer {token}`
|
||||||
|
|
||||||
|
**返回数据**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"data": {
|
||||||
|
"name": "string",
|
||||||
|
"phone": "string",
|
||||||
|
"jobTitle": ["string"],
|
||||||
|
"jobTitleId": "string",
|
||||||
|
// ... 其他用户信息
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **小程序配置**
|
||||||
|
- 需要在微信小程序后台配置服务器域名
|
||||||
|
- 需要申请手机号授权权限
|
||||||
|
|
||||||
|
2. **H5配置**
|
||||||
|
- 需要配置微信公众号的授权回调域名
|
||||||
|
- 需要引入微信JSSDK
|
||||||
|
|
||||||
|
3. **安全性**
|
||||||
|
- Token存储在本地缓存中,注意加密
|
||||||
|
- 敏感操作前需要重新验证token有效性
|
||||||
|
|
||||||
|
4. **用户体验**
|
||||||
|
- 登录弹窗不可通过点击遮罩关闭,确保用户必须做出选择
|
||||||
|
- 提供测试登录按钮方便开发调试
|
||||||
|
- 登录成功后自动刷新数据
|
||||||
|
|
||||||
|
5. **兼容性**
|
||||||
|
- 使用条件编译确保在不同平台上正常运行
|
||||||
|
- 小程序、H5、App使用不同的登录逻辑
|
||||||
|
|
||||||
|
## 测试建议
|
||||||
|
|
||||||
|
### 功能测试
|
||||||
|
1. 未登录状态点击"附近工作",应弹出登录弹窗
|
||||||
|
2. 未登录状态点击九宫格任意服务,应弹出登录弹窗
|
||||||
|
3. 未登录状态点击职位列表,应弹出登录弹窗
|
||||||
|
4. 登录成功后,能够正常访问所有功能
|
||||||
|
5. 关闭登录弹窗后,不会自动跳转到登录页
|
||||||
|
|
||||||
|
### 登录流程测试
|
||||||
|
1. 微信小程序:测试手机号授权流程
|
||||||
|
2. H5:测试微信网页授权流程
|
||||||
|
3. 测试账号登录功能(开发环境)
|
||||||
|
4. 测试登录失败的错误提示
|
||||||
|
5. 测试用户取消授权的处理
|
||||||
|
|
||||||
|
### 状态管理测试
|
||||||
|
1. 测试应用重启后登录状态的恢复
|
||||||
|
2. 测试token失效后的处理
|
||||||
|
3. 测试退出登录功能
|
||||||
|
4. 测试多次登录的状态切换
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.0.0 (2024-10-20)
|
||||||
|
- 创建微信授权登录弹窗组件
|
||||||
|
- 添加登录状态检查逻辑
|
||||||
|
- 优化用户状态管理
|
||||||
|
- 更新首页各功能的登录验证
|
||||||
|
- 完善登录流程和错误处理
|
||||||
|
|
||||||
|
## 开发者
|
||||||
|
- 开发时间: 2024-10-20
|
||||||
|
- 涉及模块: 登录模块、首页模块、用户状态管理
|
||||||
|
|
@@ -14,7 +14,7 @@
|
|||||||
<!-- <view class="chart button-click">职业图谱</view> -->
|
<!-- <view class="chart button-click">职业图谱</view> -->
|
||||||
</view>
|
</view>
|
||||||
<view class="cards" v-if="userInfo.userType !== 0">
|
<view class="cards" v-if="userInfo.userType !== 0">
|
||||||
<view class="card press-button" @click="navTo('/pages/nearby/nearby')">
|
<view class="card press-button" @click="handleNearbyClick">
|
||||||
<view class="card-title">附近工作</view>
|
<view class="card-title">附近工作</view>
|
||||||
<view class="card-text">好岗职等你来</view>
|
<view class="card-text">好岗职等你来</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -27,55 +27,55 @@
|
|||||||
|
|
||||||
<!-- 服务功能网格 -->
|
<!-- 服务功能网格 -->
|
||||||
<view class="service-grid" v-if="userInfo.userType !== 0">
|
<view class="service-grid" v-if="userInfo.userType !== 0">
|
||||||
<view class="service-item press-button" @click="navToService('service-guidance')">
|
<view class="service-item press-button" @click="handleServiceClick('service-guidance')">
|
||||||
<view class="service-icon service-icon-1">
|
<view class="service-icon service-icon-1">
|
||||||
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-title">服务指导</view>
|
<view class="service-title">服务指导</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-item press-button" @click="navToService('public-recruitment')">
|
<view class="service-item press-button" @click="handleServiceClick('public-recruitment')">
|
||||||
<view class="service-icon service-icon-2">
|
<view class="service-icon service-icon-2">
|
||||||
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-title">事业单位招录</view>
|
<view class="service-title">事业单位招录</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-item press-button" @click="navToService('resume-creation')">
|
<view class="service-item press-button" @click="handleServiceClick('resume-creation')">
|
||||||
<view class="service-icon service-icon-3">
|
<view class="service-icon service-icon-3">
|
||||||
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-title">简历制作</view>
|
<view class="service-title">简历制作</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-item press-button" @click="navToService('labor-policy')">
|
<view class="service-item press-button" @click="handleServiceClick('labor-policy')">
|
||||||
<view class="service-icon service-icon-4">
|
<view class="service-icon service-icon-4">
|
||||||
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-title">劳动政策指引</view>
|
<view class="service-title">劳动政策指引</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-item press-button" @click="navToService('skill-training')">
|
<view class="service-item press-button" @click="handleServiceClick('skill-training')">
|
||||||
<view class="service-icon service-icon-5">
|
<view class="service-icon service-icon-5">
|
||||||
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-title">技能培训信息</view>
|
<view class="service-title">技能培训信息</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-item press-button" @click="navToService('skill-evaluation')">
|
<view class="service-item press-button" @click="handleServiceClick('skill-evaluation')">
|
||||||
<view class="service-icon service-icon-6">
|
<view class="service-icon service-icon-6">
|
||||||
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-title">技能评价指引</view>
|
<view class="service-title">技能评价指引</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-item press-button" @click="navToService('question-bank')">
|
<view class="service-item press-button" @click="handleServiceClick('question-bank')">
|
||||||
<view class="service-icon service-icon-7">
|
<view class="service-icon service-icon-7">
|
||||||
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-title">题库和考试</view>
|
<view class="service-title">题库和考试</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-item press-button" @click="navToService('quality-assessment')">
|
<view class="service-item press-button" @click="handleServiceClick('quality-assessment')">
|
||||||
<view class="service-icon service-icon-8">
|
<view class="service-icon service-icon-8">
|
||||||
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-title">素质测评</view>
|
<view class="service-title">素质测评</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="service-item press-button" @click="navToService('ai-interview')">
|
<view class="service-item press-button" @click="handleServiceClick('ai-interview')">
|
||||||
<view class="service-icon service-icon-9">
|
<view class="service-icon service-icon-9">
|
||||||
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
|
||||||
</view>
|
</view>
|
||||||
@@ -304,6 +304,9 @@
|
|||||||
<!-- 筛选 -->
|
<!-- 筛选 -->
|
||||||
<select-filter ref="selectFilterModel"></select-filter>
|
<select-filter ref="selectFilterModel"></select-filter>
|
||||||
|
|
||||||
|
<!-- 微信授权登录弹窗 -->
|
||||||
|
<WxAuthLogin ref="wxAuthLoginRef" @success="handleLoginSuccess"></WxAuthLogin>
|
||||||
|
|
||||||
<!-- <view class="maskFristEntry" v-if="maskFristEntry">
|
<!-- <view class="maskFristEntry" v-if="maskFristEntry">
|
||||||
<view class="entry-content">
|
<view class="entry-content">
|
||||||
<text class="text1">左滑查看视频</text>
|
<text class="text1">左滑查看视频</text>
|
||||||
@@ -323,7 +326,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';
|
||||||
const { userInfo } = storeToRefs(useUserStore());
|
const { userInfo, hasLogin, token } = storeToRefs(useUserStore());
|
||||||
import useDictStore from '@/stores/useDictStore';
|
import useDictStore from '@/stores/useDictStore';
|
||||||
const { getTransformChildren, oneDictData } = useDictStore();
|
const { getTransformChildren, oneDictData } = useDictStore();
|
||||||
import useLocationStore from '@/stores/useLocationStore';
|
import useLocationStore from '@/stores/useLocationStore';
|
||||||
@@ -331,6 +334,7 @@ import selectFilter from '@/components/selectFilter/selectFilter.vue';
|
|||||||
import { useRecommedIndexedDBStore, jobRecommender } from '@/stores/useRecommedIndexedDBStore.js';
|
import { useRecommedIndexedDBStore, jobRecommender } from '@/stores/useRecommedIndexedDBStore.js';
|
||||||
import { useScrollDirection } from '@/hook/useScrollDirection';
|
import { useScrollDirection } from '@/hook/useScrollDirection';
|
||||||
import { useColumnCount } from '@/hook/useColumnCount';
|
import { useColumnCount } from '@/hook/useColumnCount';
|
||||||
|
import WxAuthLogin from '@/components/WxAuthLogin/WxAuthLogin.vue';
|
||||||
// 滚动状态管理
|
// 滚动状态管理
|
||||||
const shouldHideTop = ref(false);
|
const shouldHideTop = ref(false);
|
||||||
const shouldStickyFilter = ref(false);
|
const shouldStickyFilter = ref(false);
|
||||||
@@ -378,6 +382,7 @@ const loadmoreRef = ref(null);
|
|||||||
const conditionSearch = ref({});
|
const conditionSearch = ref({});
|
||||||
const waterfallcolumn = ref(2);
|
const waterfallcolumn = ref(2);
|
||||||
const maskFristEntry = ref(false);
|
const maskFristEntry = ref(false);
|
||||||
|
const wxAuthLoginRef = ref(null);
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
tabIndex: 'all',
|
tabIndex: 'all',
|
||||||
});
|
});
|
||||||
@@ -417,6 +422,37 @@ onMounted(() => {
|
|||||||
getJobRecommend('refresh');
|
getJobRecommend('refresh');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 登录检查函数
|
||||||
|
const checkLogin = () => {
|
||||||
|
const tokenValue = uni.getStorageSync('token') || '';
|
||||||
|
if (!tokenValue || !hasLogin.value) {
|
||||||
|
// 未登录,打开授权弹窗
|
||||||
|
wxAuthLoginRef.value?.open();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 登录成功回调
|
||||||
|
const handleLoginSuccess = () => {
|
||||||
|
// 登录成功后刷新数据
|
||||||
|
getJobRecommend('refresh');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理附近工作点击
|
||||||
|
const handleNearbyClick = () => {
|
||||||
|
if (checkLogin()) {
|
||||||
|
navTo('/pages/nearby/nearby');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理服务功能点击
|
||||||
|
const handleServiceClick = (serviceType) => {
|
||||||
|
if (checkLogin()) {
|
||||||
|
navToService(serviceType);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
try {
|
try {
|
||||||
if (isLoaded.value) return;
|
if (isLoaded.value) return;
|
||||||
@@ -487,21 +523,15 @@ function clearfindJob(job) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nextDetail(job) {
|
function nextDetail(job) {
|
||||||
// 登录
|
// 登录检查
|
||||||
let token = uni.getStorageSync('token') || ''; // 同步获取 缓存信息
|
if (checkLogin()) {
|
||||||
if (token) {
|
|
||||||
// 记录岗位类型,用作数据分析
|
// 记录岗位类型,用作数据分析
|
||||||
if (job.jobCategory) {
|
if (job.jobCategory) {
|
||||||
const recordData = recommedIndexDb.JobParameter(job);
|
const recordData = recommedIndexDb.JobParameter(job);
|
||||||
recommedIndexDb.addRecord(recordData);
|
recommedIndexDb.addRecord(recordData);
|
||||||
}
|
}
|
||||||
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
|
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
|
||||||
} else {
|
|
||||||
uni.redirectTo({
|
|
||||||
url: '/pages/login/login',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function navToService(serviceType) {
|
function navToService(serviceType) {
|
||||||
|
@@ -64,18 +64,22 @@ const useUserStore = defineStore("user", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const logOut = () => {
|
const logOut = (redirect = true) => {
|
||||||
hasLogin.value = false;
|
hasLogin.value = false;
|
||||||
token.value = ''
|
token.value = ''
|
||||||
resume.value = {}
|
resume.value = {}
|
||||||
userInfo.value = {}
|
userInfo.value = {}
|
||||||
role.value = {}
|
role.value = {}
|
||||||
uni.clearStorageSync('userInfo')
|
uni.removeStorageSync('userInfo')
|
||||||
uni.clearStorageSync('token')
|
uni.removeStorageSync('token')
|
||||||
|
|
||||||
|
// 只有在明确需要跳转时才跳转到登录页
|
||||||
|
if (redirect) {
|
||||||
uni.redirectTo({
|
uni.redirectTo({
|
||||||
url: '/pages/login/login',
|
url: '/pages/login/login',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getUserInfo = () => {
|
const getUserInfo = () => {
|
||||||
return new Promise((reslove, reject) => {
|
return new Promise((reslove, reject) => {
|
||||||
|
Reference in New Issue
Block a user