Merge branch 'login'

This commit is contained in:
feige996
2025-08-21 17:20:19 +08:00
23 changed files with 738 additions and 119 deletions

View File

@@ -71,6 +71,7 @@
"cSpell.words": [
"alova",
"Aplipay",
"chooseavatar",
"climblee",
"commitlint",
"dcloudio",

3
codes/README.md Normal file
View File

@@ -0,0 +1,3 @@
# 参考代码
部分代码片段,供参考。

222
codes/router.txt Normal file
View File

@@ -0,0 +1,222 @@
import { getCurrentInstance, type App } from 'vue'
import { useUserLoginStore } from '@/store/login'
import { Pages } from './pages'
import { LoginPopupViewer } from './loginPopupServices'
import Loading from './Loading'
/** 实时判断用户是否已登录(避免 computed 缓存) */
function isUserLoggedIn(): boolean {
return useUserLoginStore().isLoggedIn
}
// 路由相关配置
// 这里可以根据实际情况调整
// 例如:需要登录验证的页面等
// 以及登录页面、会员中心页面等
// 需要登录验证的页面
const authPages = [
Pages.USER_INFO_EDIT,
Pages.VIP_CENTER,
//Pages.PRODUCT_LIST,
//Pages.PRODUCT_DETAILS,
Pages.USER_ACCOUNT_SECURITY,
Pages.USER_EDIT_NICKNAME,
Pages.USER_ORDER_LIST,
Pages.USER_ORDER_DETAILS,
Pages.USER_MOBILE,
Pages.DISTRIBUTION_CENTER,
Pages.DISTRIBUTION_CENTER_DETAILS,
Pages.USER_MOBILE_CHANGE,
Pages.USER_PERSONAL_INFO,
Pages.USER_REMARK,
Pages.PRODUCT_ORDER_CONFIRM,
Pages.PRODUCT_PAY_MODE,
Pages.COUPON_CENTER,
Pages.COUPON_LIST,
Pages.CUSTOMER_SERVICE,
Pages.SHIPPING_ADDRESS_ADDED_OR_EDIT,
Pages.SHIPPING_ADDRESS_LIST,
Pages.USER_PASSWORD_CONFIG,
Pages.WITHDRAWAL,
Pages.WITHDRAWAL_RECORD_LIST,
]
/** 判断是否需要登录 */
function getBasePath(url: string): string {
const index = url.indexOf('?')
return index !== -1 ? url.substring(0, index) : url
}
function isAuthRequired(url: string): boolean {
const cleanUrl = getBasePath(url)
console.log(`URL数据源:${authPages}`)
console.log(`URL原始值: ${url}`)
console.log(`URL过滤值: ${cleanUrl}`)
return authPages.some((item) => item === cleanUrl)
}
/** 缓存跳转路径 */
function cacheRedirect(url: string) {
uni.setStorageSync('pending_redirect', url)
}
/** 读取并清除缓存跳转路径 */
function consumeRedirect(): string | null {
const url = uni.getStorageSync('pending_redirect')
uni.removeStorageSync('pending_redirect')
return url || null
}
/** 路由核心跳转方法 */
async function internalNavigate(
type: 'navigateTo' | 'redirectTo' | 'switchTab' | 'reLaunch',
url: string,
options: Record<string, any> = {},
) {
const originUrl: string = url.startsWith('/') ? url : `/${url}`
const isAuthPage = isAuthRequired(originUrl)
console.log(`[Router][${type}] 跳转到:`, originUrl, '需要登录:', isAuthPage)
console.log(`[Router][${type}] 是否登录:`, isUserLoggedIn)
// 如果需要登录但未登录,则弹出登录框
if (isAuthPage && !isUserLoggedIn()) {
cacheRedirect(originUrl)
const loginResult = await LoginPopupViewer.open()
console.log(`[Router][${type}] 登录弹窗结果:`, loginResult)
// 如果登录失败(或用户取消),中断跳转
if (!loginResult) {
console.log(`[Router][${type}] 已终止跳转,原因:用户未登录或取消登录`)
Loading.showError({ msg: '已取消登录' })
return
}
}
// 登录状态已满足,可以安全跳转
try {
switch (type) {
case 'navigateTo':
return await uniNavigateTo(originUrl, options)
case 'redirectTo':
return await uniRedirectTo(originUrl, options)
case 'switchTab':
return await uniSwitchTab(originUrl)
case 'reLaunch':
return await uniReLaunch(originUrl)
}
} catch (error) {
console.error(`[Router][${type}] 跳转失败:`, error)
}
}
/** ✅ Promise 封装 uni API **/
function uniNavigateTo(url: string, options: any) {
return new Promise((resolve, reject) => {
uni.navigateTo({
url,
...options,
success: resolve,
fail: reject,
})
})
}
function uniRedirectTo(url: string, options: any) {
return new Promise((resolve, reject) => {
uni.redirectTo({
url,
...options,
success: resolve,
fail: reject,
})
})
}
function uniSwitchTab(url: string) {
return new Promise((resolve, reject) => {
uni.switchTab({
url,
success: resolve,
fail: reject,
})
})
}
function uniReLaunch(url: string) {
return new Promise((resolve, reject) => {
uni.reLaunch({
url,
success: resolve,
fail: reject,
})
})
}
// ✅ Router API 对象
// ✅ Router API 对象
export const Router = {
// 页面跳转,支持登录鉴权
async navigateTo(opt: { url: string; requiresAuth?: boolean } & Record<string, any>) {
return await internalNavigate('navigateTo', opt.url, opt)
},
// 页面重定向,支持登录鉴权
async redirectTo(opt: { url: string; requiresAuth?: boolean } & Record<string, any>) {
return await internalNavigate('redirectTo', opt.url, opt)
},
// tab 页面切换
async switchTab(opt: { url: string }) {
return await internalNavigate('switchTab', opt.url, opt)
},
// 重新启动应用跳转
async reLaunch(opt: { url: string }) {
return await internalNavigate('reLaunch', opt.url, opt)
},
// 重定向别名
async replace(opt: { url: string; requiresAuth?: boolean } & Record<string, any>) {
return await internalNavigate('redirectTo', opt.url, opt)
},
// 返回上一级
async back(delta = 1) {
return await new Promise((resolve, reject) => {
uni.navigateBack({
delta,
success: resolve,
fail: reject,
})
})
},
consumeRedirect,
}
let cachedRouter: typeof Router | null = null
/**
* ✅ 全局安全获取 $Router 实例(推荐使用)
*/
export function useRouter(): typeof Router {
if (cachedRouter) return cachedRouter
const instance = getCurrentInstance()
if (!instance) {
throw new Error('useRouter() 必须在 setup() 或生命周期中调用')
}
const router = instance.appContext.config.globalProperties.$Router
if (!router) {
throw new Error('$Router 尚未注入,请在 main.ts 中使用 app.use(RouterPlugin)')
}
cachedRouter = router
return router
}
/** ✅ 注册为全局插件 */
export default {
install(app: App) {
app.config.globalProperties.$Router = Router
},
}

3
env/.env vendored
View File

@@ -8,9 +8,6 @@ VITE_WX_APPID = 'wxa2abb91f64032a2b'
# https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router
VITE_APP_PUBLIC_BASE=/
# 登录页面
VITE_LOGIN_URL = '/pages/login/index'
# 后台请求地址
VITE_SERVER_BASEURL = 'https://ukw0y1.laf.run'
# 后台上传地址

View File

@@ -18,6 +18,7 @@ export default uniHelper({
'src/service/app/**',
],
rules: {
'no-useless-return': 'off',
'no-console': 'off',
'no-unused-vars': 'off',
'vue/no-unused-refs': 'off',

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
import { navigateToInterceptor } from '@/router/interceptor'
import { tabbarStore } from './tabbar/store'
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
onLaunch((options) => {
@@ -17,8 +16,6 @@ onShow((options) => {
else {
navigateToInterceptor.invoke({ url: '/' })
}
// 处理直接进入路由非首页时tabbarIndex 不正确的问题
tabbarStore.setAutoCurIdx(options.path)
})
onHide(() => {
console.log('App Hide')

View File

@@ -1,50 +0,0 @@
import { onLoad } from '@dcloudio/uni-app'
import { useUserStore } from '@/store'
import { needLoginPages as _needLoginPages, getNeedLoginPages } from '@/utils'
const loginRoute = import.meta.env.VITE_LOGIN_URL
const isDev = import.meta.env.DEV
function isLogined() {
const userStore = useUserStore()
return !!userStore.userInfo.username
}
// 检查当前页面是否需要登录
export function usePageAuth() {
onLoad((options) => {
// 获取当前页面路径
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentPath = `/${currentPage.route}`
// 获取需要登录的页面列表
let needLoginPages: string[] = []
if (isDev) {
needLoginPages = getNeedLoginPages()
}
else {
needLoginPages = _needLoginPages
}
// 检查当前页面是否需要登录
const isNeedLogin = needLoginPages.includes(currentPath)
if (!isNeedLogin) {
return
}
const hasLogin = isLogined()
if (hasLogin) {
return true
}
// 构建重定向URL
const queryString = Object.entries(options || {})
.map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`)
.join('&')
const currentFullPath = queryString ? `${currentPath}?${queryString}` : currentPath
const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(currentFullPath)}`
// 重定向到登录页
uni.redirectTo({ url: redirectRoute })
})
}

View File

@@ -30,7 +30,7 @@ const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthenticati
}
catch (error) {
// 切换到登录页
await uni.reLaunch({ url: '/pages/common/login/index' })
await uni.reLaunch({ url: '/pages/login/login' })
throw error
}
},

23
src/login/README.md Normal file
View File

@@ -0,0 +1,23 @@
# 登录 说明
## 登录 2种策略
- 默认无需登录策略: DEFAULT_NO_NEED_LOGIN
- 默认需要登录策略: DEFAULT_NEED_LOGIN
### 默认无需登录策略: DEFAULT_NO_NEED_LOGIN
进入任何页面都不需要登录,只有进入到黑名单中的页面/或者页面中某些动作需要登录,才需要登录。
比如大部分2C的应用美团、今日头条、抖音等都可以直接浏览只有点赞、评论、分享等操作或者去特殊页面比如个人中心才需要登录。
### 默认需要登录策略: DEFAULT_NEED_LOGIN
进入任何页面都需要登录,只有进入到白名单中的页面,才不需要登录。默认进入应用需要先去登录页。
比如大部分2B和后台管理类的应用比如企业微信、钉钉、飞书、内部报表系统、CMS系统等都需要登录只有登录后才能使用。
### EXCLUDE_PAGE_LIST
`EXCLUDE_PAGE_LIST` 表示排除的路由列表。
`默认无需登录策略: DEFAULT_NO_NEED_LOGIN` 中,只有路由在 `EXCLUDE_PAGE_LIST` 中,才需要登录,相当于黑名单。
`默认需要登录策略: DEFAULT_NEED_LOGIN` 中,只有路由在 `EXCLUDE_PAGE_LIST` 中,才不需要登录,相当于白名单。

15
src/login/config.ts Normal file
View File

@@ -0,0 +1,15 @@
export const LOGIN_STRATEGY_MAP = {
DEFAULT_NO_NEED_LOGIN: 0, // 黑名单策略默认可以进入APP
DEFAULT_NEED_LOGIN: 1, // 白名单策略默认不可以进入APP需要强制登录
}
// 登录策略,默认使用`无需登录策略`,即默认不需要登录就可以访问
export const LOGIN_STRATEGY = LOGIN_STRATEGY_MAP.DEFAULT_NO_NEED_LOGIN
export const isNeedLogin = LOGIN_STRATEGY === LOGIN_STRATEGY_MAP.DEFAULT_NEED_LOGIN
export const LOGIN_PAGE = '/pages/login/login'
export const LOGIN_PAGE_LIST = [LOGIN_PAGE, '/pages/login/register']
// 排除在外的列表,白名单策略指白名单列表,黑名单策略指黑名单列表
export const EXCLUDE_PAGE_LIST = [
'/pages/xxx/index',
]

View File

@@ -36,6 +36,12 @@
"selectedIconPath": "static/tabbar/exampleHL.png",
"pagePath": "pages/about/about",
"text": "关于"
},
{
"iconPath": "static/tabbar/personal.png",
"selectedIconPath": "static/tabbar/personalHL.png",
"pagePath": "pages/me/me",
"text": "个人"
}
]
},
@@ -72,6 +78,29 @@
"style": {
"navigationBarTitleText": "Vue Query 请求演示"
}
},
{
"path": "pages/login/login",
"type": "page",
"layout": "default",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/login/register",
"type": "page",
"layout": "default",
"style": {
"navigationBarTitleText": "注册"
}
},
{
"path": "pages/me/me",
"type": "page",
"style": {
"navigationBarTitleText": "我的"
}
}
],
"subPackages": [

View File

@@ -17,6 +17,12 @@ import RequestComp from './components/request.vue'
// testOxlint('oxlint')
console.log('about')
function toLogin() {
uni.navigateTo({
url: `/pages/login/login?redirect=${encodeURIComponent('/pages/about/about')}`,
})
}
function gotoAlova() {
uni.navigateTo({
url: '/pages/about/alova',
@@ -41,6 +47,12 @@ onReady(() => {
console.log('onReady:', uniLayout.value) // onReady: Proxy(Object)
console.log('onReady:', uniLayout.value.testUniLayoutExposedData) // onReady: testUniLayoutExposedData
})
function gotoTabbar() {
uni.switchTab({
url: '/pages/index/index',
})
}
</script>
<template>
@@ -51,6 +63,9 @@ onReady(() => {
<view class="my-2 text-center">
<image src="/static/images/avatar.jpg" class="h-100px w-100px" />
</view>
<button class="mt-4 w-40 text-center" @click="toLogin">
点击去登录页
</button>
<RequestComp />
<view class="mb-6 h-1px bg-#eee" />
<view class="text-center">
@@ -58,6 +73,11 @@ onReady(() => {
前往 alova 示例页面
</button>
</view>
<view class="text-center">
<button type="primary" size="mini" class="w-160px" @click="gotoTabbar">
切换tabbar
</button>
</view>
<view class="text-center">
<button type="primary" size="mini" class="w-160px" @click="gotoVueQuery">
vue-query 示例页面

View File

@@ -50,6 +50,12 @@ onLoad(() => {
console.log('项目作者:', author.value)
})
function toLogin() {
uni.navigateTo({
url: '/pages/login/login',
})
}
console.log('index')
</script>
@@ -122,6 +128,9 @@ console.log('index')
https://wot-design-uni.cn
</text>
</view>
<button class="mt-4 w-40 text-center" @click="toLogin">
点击去登录页
</button>
<view class="h-6" />
</view>
</template>

10
src/pages/login/README.md Normal file
View File

@@ -0,0 +1,10 @@
# 登录注册
登录页 `login.vue` 对应路由是 `/pages/login/login`.
注册页 `register.vue` 对应路由是 `/pages/login/register`.
## 适用性
登录注册页主要适用于 `h5``App`,因为小程序通常会使用平台提供的快捷登录。
特殊情况也是可以用在 `小程序` 上的,如业务需要跨平台复用登录注册页时,所以主要还是看业务形态。

66
src/pages/login/login.vue Normal file
View File

@@ -0,0 +1,66 @@
<route lang="jsonc" type="page">
{
"layout": "default",
"style": {
"navigationBarTitleText": "登录"
}
}
</route>
<script lang="ts" setup>
import { useUserStore } from '@/store/user'
import { tabbarList } from '@/tabbar/config'
import { isPageTabbar } from '@/tabbar/store'
import { ensureDecodeURIComponent } from '@/utils'
const redirectUrl = ref('')
onLoad((options) => {
console.log('login options', options)
if (options.redirect) {
redirectUrl.value = ensureDecodeURIComponent(options.redirect)
}
else {
redirectUrl.value = tabbarList[0].pagePath
}
})
const userStore = useUserStore()
function doLogin() {
userStore.setUserInfo({
id: 1,
username: '菲鸽',
avatar: 'https://unibest.oss-cn-beijing.aliyuncs.com/avatar.png',
token: 'fake-token',
})
console.log(redirectUrl.value)
let path = redirectUrl.value
if (!path.startsWith('/')) {
path = `/${path}`
}
console.log('path:', path)
if (isPageTabbar(path)) {
uni.switchTab({
url: path,
})
}
else {
uni.redirectTo({
url: path,
})
}
}
</script>
<template>
<view class="login">
<view class="text-center">
登录页
</view>
<button class="mt-4 w-40 text-center" @click="doLogin">
点击模拟登录
</button>
</view>
</template>
<style lang="scss" scoped>
//
</style>

View File

@@ -0,0 +1,35 @@
<route lang="jsonc" type="page">
{
"layout": "default",
"style": {
"navigationBarTitleText": "注册"
}
}
</route>
<script lang="ts" setup>
function doRegister() {
uni.showToast({
title: '注册成功',
})
// 注册成功后跳转到登录页
uni.navigateTo({
url: '/pages/login/login',
})
}
</script>
<template>
<view class="login">
<view class="text-center">
注册页
</view>
<button class="mt-4 w-40 text-center" @click="doRegister">
点击模拟注册
</button>
</view>
</template>
<style lang="scss" scoped>
//
</style>

219
src/pages/me/me.vue Normal file
View File

@@ -0,0 +1,219 @@
<route lang="json5">
{
style: {
navigationBarTitleText: '我的',
},
}
</route>
<script lang="ts" setup>
import type { IUploadSuccessInfo } from '@/api/types/login'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/store'
import { useUpload } from '@/utils/uploadFile'
const userStore = useUserStore()
// 使用storeToRefs解构userInfo
const { userInfo } = storeToRefs(userStore)
const hasLogin = ref(false)
onShow((options) => {
hasLogin.value = !!uni.getStorageSync('token')
console.log('个人中心onShow', hasLogin.value, options)
hasLogin.value && useUserStore().getUserInfo()
})
// #ifndef MP-WEIXIN
// 上传头像
const { run: uploadAvatar } = useUpload<IUploadSuccessInfo>(
import.meta.env.VITE_UPLOAD_BASEURL,
{},
{
onSuccess: (res) => {
console.log('h5头像上传成功', res)
useUserStore().setUserAvatar(res.url)
},
},
)
// #endif
// 微信小程序下登录
async function handleLogin() {
// #ifdef MP-WEIXIN
// 微信登录
await userStore.wxLogin()
hasLogin.value = true
// #endif
// #ifndef MP-WEIXIN
uni.navigateTo({ url: '/pages/login/login' })
// #endif
}
// #ifdef MP-WEIXIN
// 微信小程序下选择头像事件
function onChooseAvatar(e: any) {
console.log('选择头像', e.detail)
const { avatarUrl } = e.detail
const { run } = useUpload<IUploadSuccessInfo>(
import.meta.env.VITE_UPLOAD_BASEURL,
{},
{
onSuccess: (res) => {
console.log('wx头像上传成功', res)
useUserStore().setUserAvatar(res.url)
},
},
avatarUrl,
)
run()
}
// #endif
// #ifdef MP-WEIXIN
// 微信小程序下设置用户名
function getUserInfo(e: any) {
console.log(e.detail)
}
// #endif
// 退出登录
function handleLogout() {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
// 清空用户信息
useUserStore().logout()
hasLogin.value = false
// 执行退出登录逻辑
uni.showToast({
title: '退出登录成功',
icon: 'success',
})
// #ifdef MP-WEIXIN
// 微信小程序,去首页
// uni.reLaunch({ url: '/pages/index/index' })
// #endif
// #ifndef MP-WEIXIN
// 非微信小程序,去登录页
// uni.navigateTo({ url: '/pages/login/login' })
// #endif
}
},
})
}
</script>
<template>
<view class="profile-container">
<!-- 用户信息区域 -->
<view class="user-info-section">
<!-- #ifdef MP-WEIXIN -->
<button class="avatar-button" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<image :src="userInfo.avatar" mode="scaleToFill" class="h-full w-full" />
</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="avatar-wrapper" @click="uploadAvatar">
<image :src="userInfo.avatar" mode="scaleToFill" class="h-full w-full" />
</view>
<!-- #endif -->
<view class="user-details">
<!-- #ifdef MP-WEIXIN -->
<input
v-model="userInfo.username"
type="nickname"
class="weui-input"
placeholder="请输入昵称"
>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="username">
{{ userInfo.username }}
</view>
<!-- #endif -->
<view class="user-id">
ID: {{ userInfo.id }}
</view>
</view>
</view>
<view class="mt-3 break-all px-3">
{{ JSON.stringify(userInfo, null, 2) }}
</view>
<view class="mt-20 px-3">
<view class="m-auto w-160px text-center">
<button v-if="hasLogin" type="warn" class="w-full" @click="handleLogout">
退出登录
</button>
<button v-else type="primary" class="w-full" @click="handleLogin">
登录
</button>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
/* 基础样式 */
.profile-container {
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
// background-color: #f7f8fa;
}
/* 用户信息区域 */
.user-info-section {
display: flex;
align-items: center;
padding: 40rpx;
margin: 30rpx 30rpx 20rpx;
background-color: #fff;
border-radius: 24rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.avatar-wrapper {
width: 160rpx;
height: 160rpx;
margin-right: 40rpx;
overflow: hidden;
border: 4rpx solid #f5f5f5;
border-radius: 50%;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.avatar-button {
height: 160rpx;
padding: 0;
margin-right: 40rpx;
overflow: hidden;
border: 4rpx solid #f5f5f5;
border-radius: 50%;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.user-details {
flex: 1;
}
.username {
margin-bottom: 12rpx;
font-size: 38rpx;
font-weight: 600;
color: #333;
letter-spacing: 0.5rpx;
}
.user-id {
font-size: 28rpx;
color: #666;
}
.user-created {
margin-top: 8rpx;
font-size: 24rpx;
color: #999;
}
</style>

View File

@@ -1,22 +1,13 @@
/**
* by 菲鸽 on 2024-03-06
* by 菲鸽 on 2025-08-19
* 路由拦截,通常也是登录拦截
* 可以设置路由白名单,或者黑名单,看业务需要选哪一个
* 我这里应为大部分都可以随便进入,所以使用黑名单
*/
import { useUserStore } from '@/store'
import { tabbarStore } from '@/tabbar/store'
import { needLoginPages as _needLoginPages, getLastPage, getNeedLoginPages } from '@/utils'
// TODO Check
const loginRoute = import.meta.env.VITE_LOGIN_URL
function isLogined() {
const userStore = useUserStore()
return !!userStore.userInfo.username
}
const isDev = import.meta.env.DEV
import { getLastPage } from '@/utils'
import { EXCLUDE_PAGE_LIST, isNeedLogin, LOGIN_PAGE, LOGIN_PAGE_LIST } from '../login/config'
// 黑名单登录拦截器 - (适用于大部分页面不需要登录,少部分页面需要登录)
export const navigateToInterceptor = {
@@ -24,37 +15,63 @@ export const navigateToInterceptor = {
// 增加对相对路径的处理BY 网友 @ideal
invoke({ url, query }: { url: string, query?: Record<string, string> }) {
console.log(url) // /pages/route-interceptor/index?name=feige&age=30
console.log(query) // /pages/route-interceptor/index?name=feige&age=30
if (url === undefined) {
return
}
console.log(getCurrentPages())
if (getCurrentPages().length === 0) {
return
}
let path = url.split('?')[0]
// 处理相对路径
if (!path.startsWith('/')) {
const currentPath = getLastPage().route
const currentPath = getLastPage()?.route || ''
const normalizedCurrentPath = currentPath.startsWith('/') ? currentPath : `/${currentPath}`
const baseDir = normalizedCurrentPath.substring(0, normalizedCurrentPath.lastIndexOf('/'))
path = `${baseDir}/${path}`
}
let needLoginPages: string[] = []
// 为了防止开发时出现BUG这里每次都获取一下。生产环境可以移到函数外性能更好
if (isDev) {
needLoginPages = getNeedLoginPages()
// 处理直接进入路由非首页时tabbarIndex 不正确的问题
tabbarStore.setAutoCurIdx(path)
if (LOGIN_PAGE_LIST.includes(path)) {
console.log('000')
return
}
console.log('拦截器中得到的 path:', path)
console.log('拦截器中得到的 query:', query)
if (query) {
path += `?${Object.keys(query).map(key => `${key}=${query[key]}`).join('&')}`
}
const redirectUrl = `${LOGIN_PAGE}?redirect=${encodeURIComponent(path)}`
const userStore = useUserStore()
// #region 1/2 需要登录的情况 ---------------------------
if (isNeedLogin) {
if (userStore.hasLogin) {
return
}
else {
if (EXCLUDE_PAGE_LIST.includes(path)) {
return
}
else {
uni.navigateTo({ url: redirectUrl })
}
}
}
// #endregion 1/2 需要登录的情况 ---------------------------
// #region 2/2 不需要登录的情况 ---------------------------
else {
needLoginPages = _needLoginPages
if (EXCLUDE_PAGE_LIST.includes(path)) {
uni.navigateTo({ url: redirectUrl })
}
}
const isNeedLogin = needLoginPages.includes(path)
if (!isNeedLogin) {
return true
}
const hasLogin = isLogined()
if (hasLogin) {
return true
}
tabbarStore.restorePrevIdx()
const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(url)}`
uni.navigateTo({ url: redirectRoute })
return false
// #endregion 2/2 不需要登录的情况 ---------------------------
},
}

View File

@@ -133,9 +133,11 @@ export const useUserStore = defineStore(
userToken,
login,
wxLogin,
setUserInfo,
getUserInfo,
setUserAvatar,
logout,
hasLogin: computed(() => !!userToken.value.token),
refreshToken,
}
},

View File

@@ -38,6 +38,12 @@ export const nativeTabbarList: NativeTabBarItem[] = [
pagePath: 'pages/about/about',
text: '关于',
},
{
iconPath: 'static/tabbar/personal.png',
selectedIconPath: 'static/tabbar/personalHL.png',
pagePath: 'pages/me/me',
text: '个人',
},
]
export interface CustomTabBarItem {
@@ -71,17 +77,18 @@ export const customTabbarList: CustomTabBarItem[] = [
icon: 'i-carbon-code',
// badge: 10,
},
// {
// pagePath: 'pages/mine/index',
// text: '我的',
// // 注意 iconfont 图标需要额外加上 'iconfont',如下
// iconType: 'iconfont',
// icon: 'iconfont icon-my',
// },
{
pagePath: 'pages/me/me',
text: '我的',
iconType: 'uniUi',
icon: 'contact',
},
// {
// pagePath: 'pages/index/index',
// text: '首页',
// 注意 iconfont 图标需要额外加上 'iconfont',如下
// iconType: 'iconfont',
// icon: 'iconfont icon-my',
// // 使用 image需要配置 icon + iconActive 2张图片不推荐
// // 既然已经用了自定义tabbar了就不建议用图片了所以不推荐
// iconType: 'image',

View File

@@ -1,21 +1,28 @@
import type { CustomTabBarItem } from './config'
import { tabbarList as _tabbarList } from './config'
import { tabbarList as _tabbarList, customTabbarEnable } from './config'
// TODO 1/2: 中间的鼓包tabbarItem的开关
const BULGE_ENABLE = true
/** tabbarList 里面的 path 从 pages.config.ts 得到 */
const tabbarList: CustomTabBarItem[] = _tabbarList.map(item => ({ ...item, pagePath: item.pagePath.startsWith('/') ? item.pagePath : `/${item.pagePath}` }))
const tabbarList: CustomTabBarItem[] = _tabbarList.map(item => ({
...item,
pagePath: item.pagePath.startsWith('/') ? item.pagePath : `/${item.pagePath}`,
}))
if (BULGE_ENABLE) {
if (tabbarList.length % 2 === 1) {
console.error('tabbar 数量必须是偶数,否则样式很奇怪!!')
if (customTabbarEnable && BULGE_ENABLE) {
if (tabbarList.length % 2) {
console.error('有鼓包时 tabbar 数量必须是偶数,否则样式很奇怪!!')
}
tabbarList.splice(tabbarList.length / 2, 0, {
isBulge: true,
} as CustomTabBarItem)
}
export function isPageTabbar(path: string) {
return tabbarList.some(item => item.pagePath === path)
}
/**
* 自定义 tabbar 的状态管理,原生 tabbar 无需关注本文件
* tabbar 状态,增加 storageSync 保证刷新浏览器时在正确的 tabbar 页面
@@ -30,7 +37,7 @@ const tabbarStore = reactive({
},
setAutoCurIdx(path: string) {
const index = tabbarList.findIndex(item => item.pagePath === path)
// console.log('index:', index, path)
console.log('index:', index, path)
// console.log('tabbarList:', tabbarList)
if (index === -1) {
this.setCurIdx(0)

View File

@@ -12,7 +12,7 @@ export function getLastPage() {
/**
* 获取当前页面路由的 path 路径和 redirectPath 路径
* path 如 '/pages/login/index'
* path 如 '/pages/login/login'
* redirectPath 如 '/pages/demo/base/route-interceptor'
*/
export function currRoute() {
@@ -25,8 +25,8 @@ export function currRoute() {
// 经过多端测试,只有 fullPath 靠谱,其他都不靠谱
const { fullPath } = currRoute as { fullPath: string }
// console.log(fullPath)
// eg: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序)
// eg: /pages/login/index?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5)
// eg: /pages/login/login?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序)
// eg: /pages/login/login?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5)
return getUrlObj(fullPath)
}
@@ -38,8 +38,8 @@ export function ensureDecodeURIComponent(url: string) {
}
/**
* 解析 url 得到 path 和 query
* 比如输入url: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor
* 输出: {path: /pages/login/index, query: {redirect: /pages/demo/base/route-interceptor}}
* 比如输入url: /pages/login/login?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor
* 输出: {path: /pages/login/login, query: {redirect: /pages/demo/base/route-interceptor}}
*/
export function getUrlObj(url: string) {
const [path, queryStr] = url.split('?')
@@ -110,18 +110,6 @@ export function getCurrentPageI18nKey() {
return currPage.style.navigationBarTitleText
}
/**
* 得到所有的需要登录的 pages包括主包和分包的
* 只得到 path 数组
*/
export const getNeedLoginPages = (): string[] => getAllPages('needLogin').map(page => page.path)
/**
* 得到所有的需要登录的 pages包括主包和分包的
* 只得到 path 数组
*/
export const needLoginPages: string[] = getAllPages('needLogin').map(page => page.path)
/**
* 根据微信小程序当前环境,判断应该获取的 baseUrl
*/

View File

@@ -29,6 +29,7 @@
"plugins": ["@uni-helper/uni-types/volar-plugin"]
},
"include": [
"package.json",
"src/**/*.ts",
"src/**/*.js",
"src/**/*.d.ts",