Merge branch 'login'
This commit is contained in:
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -71,6 +71,7 @@
|
||||
"cSpell.words": [
|
||||
"alova",
|
||||
"Aplipay",
|
||||
"chooseavatar",
|
||||
"climblee",
|
||||
"commitlint",
|
||||
"dcloudio",
|
||||
|
||||
3
codes/README.md
Normal file
3
codes/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 参考代码
|
||||
|
||||
部分代码片段,供参考。
|
||||
222
codes/router.txt
Normal file
222
codes/router.txt
Normal 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
3
env/.env
vendored
@@ -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'
|
||||
# 后台上传地址
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
}
|
||||
@@ -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
23
src/login/README.md
Normal 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
15
src/login/config.ts
Normal 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',
|
||||
]
|
||||
@@ -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": [
|
||||
|
||||
@@ -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 示例页面
|
||||
|
||||
@@ -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
10
src/pages/login/README.md
Normal 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
66
src/pages/login/login.vue
Normal 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>
|
||||
35
src/pages/login/register.vue
Normal file
35
src/pages/login/register.vue
Normal 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
219
src/pages/me/me.vue
Normal 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>
|
||||
@@ -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 不需要登录的情况 ---------------------------
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -133,9 +133,11 @@ export const useUserStore = defineStore(
|
||||
userToken,
|
||||
login,
|
||||
wxLogin,
|
||||
setUserInfo,
|
||||
getUserInfo,
|
||||
setUserAvatar,
|
||||
logout,
|
||||
hasLogin: computed(() => !!userToken.value.token),
|
||||
refreshToken,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"plugins": ["@uni-helper/uni-types/volar-plugin"]
|
||||
},
|
||||
"include": [
|
||||
"package.json",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.js",
|
||||
"src/**/*.d.ts",
|
||||
|
||||
Reference in New Issue
Block a user