diff --git a/.vscode/settings.json b/.vscode/settings.json index 4fbb64b..020efc6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -71,6 +71,7 @@ "cSpell.words": [ "alova", "Aplipay", + "chooseavatar", "climblee", "commitlint", "dcloudio", diff --git a/codes/README.md b/codes/README.md new file mode 100644 index 0000000..38a29c3 --- /dev/null +++ b/codes/README.md @@ -0,0 +1,3 @@ +# 参考代码 + +部分代码片段,供参考。 \ No newline at end of file diff --git a/codes/router.txt b/codes/router.txt new file mode 100644 index 0000000..e9e28d5 --- /dev/null +++ b/codes/router.txt @@ -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 = {}, +) { + 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) { + return await internalNavigate('navigateTo', opt.url, opt) + }, + + // 页面重定向,支持登录鉴权 + async redirectTo(opt: { url: string; requiresAuth?: boolean } & Record) { + 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) { + 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 + }, +} diff --git a/env/.env b/env/.env index c6d37d8..32c47d0 100644 --- a/env/.env +++ b/env/.env @@ -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' # 后台上传地址 diff --git a/eslint.config.mjs b/eslint.config.mjs index 7383bed..328c179 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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', diff --git a/src/App.vue b/src/App.vue index e948c0f..21a4f8f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,7 +1,6 @@ diff --git a/src/pages/login/README.md b/src/pages/login/README.md new file mode 100644 index 0000000..adf468b --- /dev/null +++ b/src/pages/login/README.md @@ -0,0 +1,10 @@ +# 登录注册 + +登录页 `login.vue` 对应路由是 `/pages/login/login`. +注册页 `register.vue` 对应路由是 `/pages/login/register`. + +## 适用性 + +登录注册页主要适用于 `h5` 和 `App`,因为小程序通常会使用平台提供的快捷登录。 + +特殊情况也是可以用在 `小程序` 上的,如业务需要跨平台复用登录注册页时,所以主要还是看业务形态。 diff --git a/src/pages/login/login.vue b/src/pages/login/login.vue new file mode 100644 index 0000000..0601da1 --- /dev/null +++ b/src/pages/login/login.vue @@ -0,0 +1,66 @@ + +{ + "layout": "default", + "style": { + "navigationBarTitleText": "登录" + } +} + + + + + + + diff --git a/src/pages/login/register.vue b/src/pages/login/register.vue new file mode 100644 index 0000000..b28cfab --- /dev/null +++ b/src/pages/login/register.vue @@ -0,0 +1,35 @@ + +{ + "layout": "default", + "style": { + "navigationBarTitleText": "注册" + } +} + + + + + + + diff --git a/src/pages/me/me.vue b/src/pages/me/me.vue new file mode 100644 index 0000000..67fce5c --- /dev/null +++ b/src/pages/me/me.vue @@ -0,0 +1,219 @@ + +{ + style: { + navigationBarTitleText: '我的', + }, +} + + + + + + + diff --git a/src/router/interceptor.ts b/src/router/interceptor.ts index d4f59e3..0f8b398 100644 --- a/src/router/interceptor.ts +++ b/src/router/interceptor.ts @@ -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 }) { 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 不需要登录的情况 --------------------------- }, } diff --git a/src/store/user.ts b/src/store/user.ts index f020f99..aadc52c 100644 --- a/src/store/user.ts +++ b/src/store/user.ts @@ -133,9 +133,11 @@ export const useUserStore = defineStore( userToken, login, wxLogin, + setUserInfo, getUserInfo, setUserAvatar, logout, + hasLogin: computed(() => !!userToken.value.token), refreshToken, } }, diff --git a/src/tabbar/config.ts b/src/tabbar/config.ts index 8a28010..9fde129 100644 --- a/src/tabbar/config.ts +++ b/src/tabbar/config.ts @@ -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', diff --git a/src/tabbar/store.ts b/src/tabbar/store.ts index 86e0db1..b3600af 100644 --- a/src/tabbar/store.ts +++ b/src/tabbar/store.ts @@ -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) diff --git a/src/utils/index.ts b/src/utils/index.ts index 1d6dc03..9ef3987 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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 */ diff --git a/tsconfig.json b/tsconfig.json index c95ea2f..0a76bae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,6 +29,7 @@ "plugins": ["@uni-helper/uni-types/volar-plugin"] }, "include": [ + "package.json", "src/**/*.ts", "src/**/*.js", "src/**/*.d.ts",