hookehuyr

feat(权限): 实现权限管理系统并集成到各页面

添加权限管理系统,包括权限检查工具函数和用户状态管理
- 创建权限检查工具函数 checkPermission 和权限类型枚举
- 实现用户状态管理 store 用于集中管理用户信息和权限状态
- 在各页面集成权限检查,包括个人中心、卖车、购买等功能
- 添加权限使用示例文档
- 重构导航逻辑使用权限检查替代原有验证方式
......@@ -56,10 +56,13 @@
import { ref, onMounted } from 'vue'
import Taro from '@tarojs/taro'
import { Home, Category, Comment, My } from '@nutui/icons-vue-taro'
import { getProfileAPI } from '@/api/index';
import { useUserStore } from '@/stores/user'
import { checkPermission, PERMISSION_TYPES } from '@/utils/permission'
// 当前激活的tab
const activeTab = ref('')
// 用户状态管理
const userStore = useUserStore()
/**
* 获取当前页面路径并设置激活状态
......@@ -88,22 +91,23 @@ const getCurrentPage = () => {
* 导航到指定页面
* @param {string} url - 页面路径
*/
const navigateTo = (url) => {
if ((url === '/pages/profile/index' || url === '/pages/sell/index') && !is_auth.value) {
Taro.showToast({
title: '请先完善个人信息',
icon: 'none',
duration: 2000,
success: () => {
setTimeout(() => {
Taro.navigateTo({
url: '/pages/register/index'
})
}, 2000);
}
})
return
const navigateTo = async (url) => {
// 定义需要权限验证的页面
const permissionPages = {
'/pages/profile/index': PERMISSION_TYPES.PROFILE,
'/pages/sell/index': PERMISSION_TYPES.SELL_CAR,
'/pages/messages/index': PERMISSION_TYPES.MESSAGE
}
// 检查是否需要权限验证
const permissionType = permissionPages[url]
if (permissionType) {
const hasPermission = await checkPermission(permissionType)
if (!hasPermission) {
return
}
}
Taro.reLaunch({
url
})
......@@ -129,27 +133,10 @@ const getIconColor = (tab) => {
return activeTab.value === tab ? '#f97316' : '#6b7280'
}
const userInfo = ref({
avatar: '',
nickname: '',
phone: '',
gender: '',
school: '',
birthday: '',
});
const is_auth = ref(false);
onMounted(async () => {
getCurrentPage()
// 获取当前用户的信息
const user = await getProfileAPI();
if (user.code) {
userInfo.value = user.data
if (userInfo.value && userInfo.value.phone) {
is_auth.value = true
}
}
// 初始化用户信息
await userStore.fetchUserInfo()
})
</script>
......
<!--
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-08 17:46:08
* @LastEditTime: 2025-07-09 10:44:50
* @FilePath: /jgdl/src/pages/auth/index.vue
* @Description: 文件描述
-->
......@@ -36,7 +36,7 @@ export default {
})
request.post('/srv/?a=openid', {
code: res.code,
openid: 'h-001'
openid: 'h-002'
// openid: 'o5NFZ5cFQtLRy3aVHaZMLkjHFusI'
// openid: 'o5NFZ5TpgG4FwYursGCLjcUJH2ak'
// openid: 'o5NFZ5cqroPYwawCp8FEOxewtgnw'
......
<!--
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-08 15:51:46
* @LastEditTime: 2025-07-09 10:42:01
* @FilePath: /jgdl/src/pages/productDetail/index.vue
* @Description: 商品详情页
-->
......@@ -266,6 +266,7 @@ import { Share, Heart1, HeartFill, Message } from '@nutui/icons-vue-taro'
import payCard from '@/components/payCard.vue'
import avatarImg from '@/assets/images/avatar.png'
import { getCurrentPageParam } from "@/utils/weapp"
import { checkPermission, PERMISSION_TYPES } from '@/utils/permission'
// 默认头像
const defaultAvatar = 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
......@@ -400,8 +401,11 @@ const copyWechat = () => {
/**
* 联系卖家
*/
const handleContactSeller = () => {
showContactModal.value = true
const handleContactSeller = async () => {
const hasPermission = await checkPermission(PERMISSION_TYPES.CONTACT_SELLER)
if (hasPermission) {
showContactModal.value = true
}
}
/**
......@@ -448,12 +452,15 @@ const sendMessageToSeller = () => {
/**
* 购买商品
*/
const handlePurchase = () => {
onPay({
id: product.value.id,
remain_time: 1800, // 30分钟
price: product.value.price
})
const handlePurchase = async () => {
const hasPermission = await checkPermission(PERMISSION_TYPES.BUY_CAR)
if (hasPermission) {
onPay({
id: product.value.id,
remain_time: 1800, // 30分钟
price: product.value.price
})
}
}
/**
......
......@@ -78,30 +78,24 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getProfileAPI } from '@/api/index'
import { computed } from 'vue'
import { Heart, Clock, Notice, Cart, Message, Tips, Right, StarN } from '@nutui/icons-vue-taro'
import Taro, { useDidShow } from '@tarojs/taro'
import TabBar from '@/components/TabBar.vue'
import { useUserStore } from '@/stores/user'
import { checkPermission, PERMISSION_TYPES } from '@/utils/permission'
// 默认头像
const defaultAvatar = 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
// 用户信息
const userInfo = ref({
nickname: '张同学',
phone: '138****8888',
avatar_url: '',
follower_count: 0,
order_count: 0,
favorite_count: 0
})
// 用户状态管理
const userStore = useUserStore()
// 用户信息计算属性
const userInfo = computed(() => userStore.userInfo)
useDidShow(async () => {
const user = await getProfileAPI()
if (user.code) {
userInfo.value = user.data
}
await userStore.fetchUserInfo()
})
/**
......@@ -116,37 +110,49 @@ const onEditProfile = () => {
/**
* 我的关注
*/
const onMyFavorites = () => {
Taro.navigateTo({
url: '/pages/myFavorites/index'
})
const onMyFavorites = async () => {
const hasPermission = await checkPermission(PERMISSION_TYPES.BASIC_INFO)
if (hasPermission) {
Taro.navigateTo({
url: '/pages/myFavorites/index'
})
}
}
/**
* 订单管理
*/
const onOrderManagement = () => {
Taro.navigateTo({
url: '/pages/myOrders/index'
})
const onOrderManagement = async () => {
const hasPermission = await checkPermission(PERMISSION_TYPES.ORDER_MANAGEMENT)
if (hasPermission) {
Taro.navigateTo({
url: '/pages/myOrders/index'
})
}
}
/**
* 我的消息
*/
const onMessages = () => {
Taro.navigateTo({
url: '/pages/messages/index'
})
const onMessages = async () => {
const hasPermission = await checkPermission(PERMISSION_TYPES.MESSAGE)
if (hasPermission) {
Taro.navigateTo({
url: '/pages/messages/index'
})
}
}
/**
* 我卖的车
*/
const onMyCar = () => {
Taro.navigateTo({
url: '/pages/myCar/index'
})
const onMyCar = async () => {
const hasPermission = await checkPermission(PERMISSION_TYPES.SELL_CAR)
if (hasPermission) {
Taro.navigateTo({
url: '/pages/myCar/index'
})
}
}
/**
......
......@@ -452,11 +452,17 @@ const handleRegister = async () => {
const result = await updateProfileAPI(formData)
if (result.code !== 0) {
showToast(result.msg || '注册失败', 'error')
Taro.showToast({
title: result.msg || '注册失败',
icon: 'error'
})
return
}
showToast('注册成功', 'success')
Taro.showToast({
title: '注册成功',
icon: 'success'
})
setTimeout(() => {
// 注册成功后跳转到登录页面或首页
......@@ -464,7 +470,10 @@ const handleRegister = async () => {
}, 1500)
} catch (error) {
showToast('注册失败,请重试', 'error')
Taro.showToast({
title: '注册失败,请重试',
icon: 'error'
})
} finally {
isRegistering.value = false
}
......
......@@ -316,6 +316,7 @@ import { Plus, Right, Location, Close, RectLeft } from '@nutui/icons-vue-taro'
import Taro from '@tarojs/taro'
import BASE_URL from '@/utils/config';
import BrandModelPicker from '@/components/BrandModelPicker.vue'
import { checkPermission, PERMISSION_TYPES } from '@/utils/permission'
import './index.less'
const themeVars = ref({
......@@ -588,23 +589,23 @@ const showSchoolPicker = () => {
/**
* 显示品牌选择器
*/
const showBrandPicker = () => {
brandPickerVisible.value = true
}
// const showBrandPicker = () => {
// brandPickerVisible.value = true
// }
/**
* 显示型号选择器
*/
const showModelPicker = () => {
if (!formData.brand) {
Taro.showToast({
title: '请先选择品牌',
icon: 'none'
})
return
}
modelPickerVisible.value = true
}
// const showModelPicker = () => {
// if (!formData.brand) {
// Taro.showToast({
// title: '请先选择品牌',
// icon: 'none'
// })
// return
// }
// modelPickerVisible.value = true
// }
/**
* 显示年份选择器
......@@ -897,7 +898,13 @@ const loadCarData = async () => {
}
// 页面加载时执行
onMounted(() => {
onMounted(async () => {
// 检查卖车权限
const hasPermission = await checkPermission(PERMISSION_TYPES.SELL_CAR)
if (!hasPermission) {
return
}
if (isEditMode.value) {
loadCarData()
}
......
/*
* @Date: 2025-01-08 18:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-09 10:34:50
* @FilePath: /jgdl/src/stores/user.js
* @Description: 用户状态管理
*/
import { defineStore } from 'pinia'
import { getProfileAPI } from '@/api/index'
import Taro from '@tarojs/taro'
export const useUserStore = defineStore('user', {
state: () => {
return {
userInfo: {
avatar: '',
nickname: '',
phone: '',
gender: '',
school: '',
birthday: '',
avatar_url: '',
follower_count: 0,
order_count: 0,
favorite_count: 0
},
isAuthenticated: false,
isLoading: false
}
},
getters: {
/**
* 检查用户是否已完善基本信息(主要是手机号)
*/
hasCompleteProfile: (state) => {
return !!(state.userInfo.phone && state.userInfo.phone.trim())
},
/**
* 检查用户是否已认证
*/
isUserAuthenticated: (state) => {
return state.isAuthenticated && state.hasCompleteProfile
},
/**
* 获取用户显示名称
*/
displayName: (state) => {
return state.userInfo.nickname || '用户'
}
},
actions: {
/**
* 获取用户信息
*/
async fetchUserInfo() {
this.isLoading = true
try {
const response = await getProfileAPI()
if (response.code) {
this.userInfo = { ...this.userInfo, ...response.data }
this.isAuthenticated = true
return true
} else {
this.isAuthenticated = false
return false
}
} catch (error) {
console.error('获取用户信息失败:', error)
this.isAuthenticated = false
return false
} finally {
this.isLoading = false
}
},
/**
* 更新用户信息
* @param {Object} newUserInfo - 新的用户信息
*/
updateUserInfo(newUserInfo) {
this.userInfo = { ...this.userInfo, ...newUserInfo }
},
/**
* 清除用户信息(登出)
*/
clearUserInfo() {
this.userInfo = {
avatar: '',
nickname: '',
phone: '',
gender: '',
school: '',
birthday: '',
avatar_url: '',
follower_count: 0,
order_count: 0,
favorite_count: 0
}
this.isAuthenticated = false
},
/**
* 检查并处理权限验证
* @param {Object} options - 配置选项
* @param {string} options.redirectUrl - 权限验证失败时的跳转地址
* @param {string} options.message - 提示消息
* @param {Function} options.onSuccess - 验证成功的回调
* @param {Function} options.onFail - 验证失败的回调
* @returns {Promise<boolean>} 是否通过权限验证
*/
async checkPermission(options = {}) {
const {
redirectUrl = '/pages/register/index',
message = '请先完善个人信息',
onSuccess,
onFail
} = options
// 如果用户信息为空,先尝试获取
if (!this.isAuthenticated) {
await this.fetchUserInfo()
}
// 检查是否有完整的用户信息
if (this.hasCompleteProfile) {
onSuccess && onSuccess()
return true
} else {
// 权限验证失败,显示提示并跳转
Taro.showToast({
title: message,
icon: 'none',
duration: 2000,
success: () => {
setTimeout(() => {
Taro.navigateTo({
url: redirectUrl
})
}, 2000)
}
})
onFail && onFail()
return false
}
}
}
})
# 权限管理系统使用示例
本文档展示了如何在项目中使用新的权限管理系统。
## 基本用法
### 1. 在页面中检查权限
```javascript
import { checkPermission, PERMISSION_TYPES } from '@/utils/permission'
// 检查卖车权限
const handleSellCar = async () => {
const hasPermission = await checkPermission(PERMISSION_TYPES.SELL_CAR)
if (hasPermission) {
// 执行卖车操作
Taro.navigateTo({ url: '/pages/sell/index' })
}
}
// 检查购买权限
const handleBuyCar = async () => {
const hasPermission = await checkPermission(PERMISSION_TYPES.BUY_CAR)
if (hasPermission) {
// 执行购买操作
onPay({ id: carId, price: price })
}
}
```
### 2. 使用用户状态管理
```javascript
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 获取用户信息
await userStore.fetchUserInfo()
// 检查用户是否已认证
if (userStore.isAuthenticated) {
// 用户已认证
}
// 检查用户资料是否完整
if (userStore.hasCompleteProfile) {
// 用户资料完整
}
```
### 3. 批量权限检查
```javascript
import { checkMultiplePermissions, PERMISSION_TYPES } from '@/utils/permission'
const permissions = await checkMultiplePermissions([
PERMISSION_TYPES.SELL_CAR,
PERMISSION_TYPES.ORDER_MANAGEMENT,
PERMISSION_TYPES.MESSAGE
])
if (permissions.SELL_CAR) {
// 有卖车权限
}
if (permissions.ORDER_MANAGEMENT) {
// 有订单管理权限
}
```
### 4. 自定义权限检查
```javascript
import { checkUserFields } from '@/utils/permission'
// 检查用户是否填写了特定字段
const hasRequiredFields = await checkUserFields(['phone', 'school', 'nickname'])
if (hasRequiredFields) {
// 用户已填写所有必需字段
}
```
## 权限类型说明
- `BASIC_INFO`: 基础信息权限(需要手机号)
- `SELL_CAR`: 卖车权限(需要手机号)
- `BUY_CAR`: 买车权限(需要手机号)
- `CONTACT_SELLER`: 联系卖家权限(需要手机号)
- `ORDER_MANAGEMENT`: 订单管理权限(需要手机号)
- `MESSAGE`: 消息权限(需要手机号)
- `PROFILE`: 个人资料权限(需要手机号)
- `COMPLETE_PROFILE`: 完整资料权限(需要手机号、学校、昵称)
## 扩展权限类型
如需添加新的权限类型,请在 `/src/utils/permission.js` 中:
1.`PERMISSION_TYPES` 中添加新类型
2.`PERMISSION_CONFIG` 中配置权限要求
3. 根据需要自定义提示信息和跳转逻辑
```javascript
// 添加新权限类型
export const PERMISSION_TYPES = {
// ... 现有类型
NEW_FEATURE: 'NEW_FEATURE'
}
// 配置权限要求
export const PERMISSION_CONFIG = {
// ... 现有配置
[PERMISSION_TYPES.NEW_FEATURE]: {
requiredFields: ['phone', 'realName'], // 需要的字段
message: '使用此功能需要完善手机号和真实姓名',
redirectUrl: '/pages/register/index'
}
}
```
## 注意事项
1. 权限检查是异步操作,请使用 `await``.then()`
2. 权限检查失败时会自动显示提示并跳转到注册页面
3. 用户状态会自动缓存,避免重复请求
4. 建议在页面加载时预先获取用户信息以提升用户体验
\ No newline at end of file
/*
* @Date: 2025-01-08 18:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-09 10:49:50
* @FilePath: /jgdl/src/utils/permission.js
* @Description: 权限控制工具函数
*/
import { useUserStore } from '@/stores/user'
import Taro from '@tarojs/taro'
/**
* 权限类型枚举
*/
export const PERMISSION_TYPES = {
// 基础权限:需要完善手机号等基本信息
BASIC_INFO: 'basic_info',
// 卖车权限:需要完善基本信息
SELL_CAR: 'sell_car',
// 买车权限:需要完善基本信息
BUY_CAR: 'buy_car',
// 个人中心权限:需要完善基本信息
PROFILE: 'profile',
// 订单管理权限:需要完善基本信息
ORDER_MANAGEMENT: 'order_management',
// 消息权限:需要完善基本信息
// MESSAGE: 'message'
}
/**
* 权限配置
*/
const PERMISSION_CONFIG = {
[PERMISSION_TYPES.BASIC_INFO]: {
message: '请先完善个人信息',
redirectUrl: '/pages/register/index',
checkFields: ['phone']
},
[PERMISSION_TYPES.SELL_CAR]: {
message: '发布车源需要先完善个人信息',
redirectUrl: '/pages/register/index',
checkFields: ['phone']
},
[PERMISSION_TYPES.BUY_CAR]: {
message: '购买车辆需要先完善个人信息',
redirectUrl: '/pages/register/index',
checkFields: ['phone']
},
[PERMISSION_TYPES.PROFILE]: {
message: '访问个人中心需要先完善个人信息',
redirectUrl: '/pages/register/index',
checkFields: ['phone']
},
[PERMISSION_TYPES.ORDER_MANAGEMENT]: {
message: '查看订单需要先完善个人信息',
redirectUrl: '/pages/register/index',
checkFields: ['phone']
},
// [PERMISSION_TYPES.MESSAGE]: {
// message: '查看消息需要先完善个人信息',
// redirectUrl: '/pages/register/index',
// checkFields: ['phone']
// }
}
/**
* 检查用户是否满足指定字段的要求
* @param {Object} userInfo - 用户信息
* @param {Array} checkFields - 需要检查的字段
* @returns {boolean} 是否满足要求
*/
function checkUserFields(userInfo, checkFields) {
return checkFields.every(field => {
const value = userInfo[field]
return value && value.toString().trim() !== ''
})
}
/**
* 权限检查函数
* @param {string} permissionType - 权限类型
* @param {Object} options - 配置选项
* @param {string} options.message - 自定义提示消息
* @param {string} options.redirectUrl - 自定义跳转地址
* @param {Array} options.checkFields - 自定义检查字段
* @param {Function} options.onSuccess - 验证成功回调
* @param {Function} options.onFail - 验证失败回调
* @param {boolean} options.showToast - 是否显示提示(默认true)
* @param {boolean} options.autoRedirect - 是否自动跳转(默认true)
* @returns {Promise<boolean>} 是否通过权限验证
*/
export async function checkPermission(permissionType, options = {}) {
const userStore = useUserStore()
// 获取权限配置
const config = PERMISSION_CONFIG[permissionType] || PERMISSION_CONFIG[PERMISSION_TYPES.BASIC_INFO]
// 合并配置
const finalConfig = {
message: options.message || config.message,
redirectUrl: options.redirectUrl || config.redirectUrl,
checkFields: options.checkFields || config.checkFields,
showToast: options.showToast !== false,
autoRedirect: options.autoRedirect !== false,
onSuccess: options.onSuccess,
onFail: options.onFail
}
try {
// 如果用户信息为空,先尝试获取
if (!userStore.isAuthenticated) {
await userStore.fetchUserInfo()
}
// 检查用户字段
const hasPermission = checkUserFields(userStore.userInfo, finalConfig.checkFields)
if (hasPermission) {
// 权限验证成功
finalConfig.onSuccess && finalConfig.onSuccess()
return true
} else {
// 权限验证失败
if (finalConfig.showToast) {
Taro.showToast({
title: finalConfig.message,
icon: 'none',
duration: 2000,
success: () => {
if (finalConfig.autoRedirect) {
setTimeout(() => {
Taro.navigateTo({
url: finalConfig.redirectUrl
})
}, 2000)
}
}
})
} else if (finalConfig.autoRedirect) {
Taro.navigateTo({
url: finalConfig.redirectUrl
})
}
finalConfig.onFail && finalConfig.onFail()
return false
}
} catch (error) {
console.error('权限检查失败:', error)
finalConfig.onFail && finalConfig.onFail(error)
return false
}
}
/**
* 权限装饰器函数,用于包装需要权限验证的函数
* @param {string} permissionType - 权限类型
* @param {Object} options - 配置选项
* @returns {Function} 装饰器函数
*/
export function requirePermission(permissionType, options = {}) {
return function(target, propertyKey, descriptor) {
const originalMethod = descriptor.value
descriptor.value = async function(...args) {
const hasPermission = await checkPermission(permissionType, options)
if (hasPermission) {
return originalMethod.apply(this, args)
}
}
return descriptor
}
}
/**
* 创建需要权限验证的导航函数
* @param {string} url - 目标页面路径
* @param {string} permissionType - 权限类型
* @param {Object} options - 配置选项
* @returns {Function} 导航函数
*/
export function createPermissionNavigate(url, permissionType, options = {}) {
return async function() {
const hasPermission = await checkPermission(permissionType, options)
if (hasPermission) {
Taro.navigateTo({ url })
}
}
}
/**
* 批量权限检查
* @param {Array} permissions - 权限类型数组
* @param {Object} options - 配置选项
* @returns {Promise<boolean>} 是否全部通过权限验证
*/
export async function checkMultiplePermissions(permissions, options = {}) {
const results = await Promise.all(
permissions.map(permission =>
checkPermission(permission, { ...options, showToast: false, autoRedirect: false })
)
)
const allPassed = results.every(result => result)
if (!allPassed && options.showToast !== false) {
const config = PERMISSION_CONFIG[permissions[0]] || PERMISSION_CONFIG[PERMISSION_TYPES.BASIC_INFO]
Taro.showToast({
title: options.message || config.message,
icon: 'none',
duration: 2000
})
}
return allPassed
}
/**
* 权限中间件,用于页面级别的权限控制
* @param {string} permissionType - 权限类型
* @param {Object} options - 配置选项
* @returns {Function} 中间件函数
*/
export function permissionMiddleware(permissionType, options = {}) {
return async function() {
const hasPermission = await checkPermission(permissionType, {
...options,
autoRedirect: true
})
if (!hasPermission) {
// 如果没有权限,阻止页面继续加载
throw new Error('权限验证失败')
}
return hasPermission
}
}