فهرست منبع

feat: 实现未登录状态权限管控,浏览后登录的审核合规方案

- 新增独立登录页面(pages/login/login),微信手机号一键登录,登录后自动返回来源页
- 新增auth守卫工具(utils/auth.ts),提供isAuthenticated/requireAuth统一鉴权入口
- 首页(map.vue)移除内嵌登录按钮,未登录可正常浏览地图和电站列表
- 用户页(user.vue)未登录时点击菜单项跳转登录页而非内嵌授权
- 自定义tabBar: 扫码充电/个人中心未登录时跳转登录页,充电地图始终允许
- charge-station组件: 收藏/领券中心操作须登录
- 子包页面(camera/ordering/appointment/orders/collect/profile/wallet/wallet-recharge) onLoad时检查登录态
- fetchCollectList无token时静默返回null,避免未登录时触发鉴权报错
- list.vue未登录时仍可浏览电站列表,跳过收藏数据加载

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 1 روز پیش
والد
کامیت
4601fe69a4

+ 3 - 0
charge-front/src/api/user.ts

@@ -30,6 +30,9 @@ export function fetchRightsAndCoupons() {
 }
 
 export function fetchCollectList() {
+  if (!getApp<any>().globalData.token) {
+    return Promise.resolve(null);
+  }
   if (getApp<any>().globalData.collectIds) {
     return Promise.resolve(getApp<any>().globalData.collectIds);
   }

+ 7 - 0
charge-front/src/components/charge-station/charge-station.vue

@@ -113,6 +113,7 @@
 
 <script lang="ts">
 import {addCollectList, fetchCollectList} from "../../api/user";
+import { requireAuth } from "../../utils/auth";
 import StationCoupon from "@/components/station-coupon/station-coupon.vue";
 
 export default {
@@ -170,6 +171,9 @@ export default {
   },
   methods: {
     toCouponCenter() {
+      if (!requireAuth()) {
+        return;
+      }
       const {address, latitude, longitude, activityList} = this;
       getApp<any>().globalData.lastData.station = {
         address,
@@ -195,6 +199,9 @@ export default {
       });
     },
     collect() {
+      if (!requireAuth()) {
+        return;
+      }
       addCollectList(Number(this.sId)).then((collected) => {
         collected &&
         uni.vibrateShort &&

+ 12 - 10
charge-front/src/custom-tab-bar/index.js

@@ -1,4 +1,5 @@
-import { fetchToken, onLogin } from "../api/auth";
+import { fetchToken } from "../api/auth";
+import { isAuthenticated, requireAuth } from "../utils/auth";
 
 Component({
   data: {
@@ -21,7 +22,7 @@ Component({
   },
   lifetimes: {
     ready() {
-      fetchToken().then((token) => {
+      fetchToken().then(() => {
         const pages = getCurrentPages();
         const index = this.data.tabs.findIndex(
           (item) => item.path === `/${pages[pages.length - 1].route}`
@@ -29,15 +30,7 @@ Component({
         this.setData({
           hidden: false,
           tabIndex: index,
-          token,
         });
-        if (!token) {
-          onLogin((token) => {
-            this.setData({
-              token,
-            });
-          });
-        }
       });
     },
   },
@@ -45,6 +38,15 @@ Component({
     switchTab(e) {
       const data = e.currentTarget.dataset;
       const url = data.path;
+      const index = data.index;
+
+      if (index === 1 || index === 2) {
+        if (!isAuthenticated()) {
+          requireAuth(url);
+          return;
+        }
+      }
+
       if (/camera/.test(url)) {
         wx.navigateTo({
           url,

+ 4 - 0
charge-front/src/pages-charge/appointment/appointment.vue

@@ -325,6 +325,7 @@ import PriceDesc from "../machines/price-desc/price-desc.vue";
 import StationChargeCoupon from "@/components/station-charge-coupon/station-charge-coupon.vue";
 import {format} from "../utils/date";
 import {redirect, to} from "../../utils/navigate";
+import { requireAuth } from "@/utils/auth";
 
 
 const DAY = 24 * 60 * 60 * 1000;
@@ -844,6 +845,9 @@ const fetchUserStationDefaultRightsAndCoupon = () => {
 }
 
 onLoad((_options: any) => {
+  if (!requireAuth()) {
+    return;
+  }
   // sn=SN100523042860091 测试环境
   console.log("options", _options);
   let {sn, connectorId, equipmentId} = _options

+ 4 - 0
charge-front/src/pages-charge/camera/camera.vue

@@ -82,6 +82,7 @@
 import { ref } from "vue";
 import { isDevTool } from "../../utils/device";
 import { deCode } from "../../utils/code";
+import { requireAuth } from "@/utils/auth";
 import { back, redirect } from "../../utils/navigate";
 import { onLoad, onReady } from "@dcloudio/uni-app";
 import { fetchChargeStatus, searchQRCode } from "../../api/charge";
@@ -92,6 +93,9 @@ const cameraFlash = ref("off");
 const backStyle = ref("");
 
 onLoad(() => {
+  if (!requireAuth()) {
+    return;
+  }
   const menuButtonRect = uni.getMenuButtonBoundingClientRect();
   backStyle.value = `left:12px;top:${menuButtonRect.top + 6}px;`;
   uni.showLoading({

+ 4 - 0
charge-front/src/pages-charge/ordering/ordering.vue

@@ -93,6 +93,7 @@ import { cancelCharge, fetchChargeStatus, startCharge } from "../../api/charge";
 import { ref } from "vue";
 import { format } from "../utils/date";
 import { to, reLaunch } from "@/utils/navigate";
+import { requireAuth } from "@/utils/auth";
 
 let timer: any;
 let statusTimer: any;
@@ -611,6 +612,9 @@ const onImgLoad = () => {
 };
 
 onLoad((_options: any) => {
+  if (!requireAuth()) {
+    return;
+  }
   options.value = _options;
 });
 onHide(() => {

+ 4 - 0
charge-front/src/pages-charge/orders/orders.vue

@@ -131,6 +131,7 @@ import { fetchInvoiceList } from "../../api/index";
 import { useInfiniteScroll } from "../../utils/infinite-scroll";
 import { ref } from "vue";
 import { to } from "@/utils/navigate";
+import { requireAuth } from "@/utils/auth";
 
 const isInvoice = ref(false);
 const isInvoiceing = ref(false);
@@ -249,6 +250,9 @@ const checkPage = () => {
 };
 
 onLoad(() => {
+  if (!requireAuth()) {
+    return;
+  }
   infiniteScroller.refresh();
 });
 onShow(() => {

+ 4 - 0
charge-front/src/pages-user/collect/collect.vue

@@ -34,9 +34,13 @@
 import {_fetchStations, _getDistance, fetchStationByIds} from "../../api/charge";
 import { fetchCollectList } from "../../api/user";
 import { onLoad } from "@dcloudio/uni-app";
+import { requireAuth } from "@/utils/auth";
 import { ref } from "vue";
 const list = ref<any[]>();
 onLoad(() => {
+  if (!requireAuth()) {
+    return;
+  }
   fetchCollectList().then((stationIdList) => {
     if (stationIdList) {
       let {latitude, longitude} = getApp<any>().globalData.fetchLocation

+ 4 - 0
charge-front/src/pages-user/profile/profile.vue

@@ -46,6 +46,7 @@ import { clearToken } from "../../api/auth";
 import { fetchProfile, updateProfile, logout } from "../../api/user";
 import { upload } from "../utils/uploader";
 import { onLoad, onShow } from "@dcloudio/uni-app";
+import { requireAuth } from "@/utils/auth";
 import { ref } from "vue";
 const avatar = ref<string>();
 const menu = ref<any[]>([]);
@@ -229,6 +230,9 @@ const errorHandle = (e: any) => {
 };
 
 onLoad(() => {
+  if (!requireAuth()) {
+    return;
+  }
   if (getApp<any>().globalData.user) {
     const user = getApp<any>().globalData.user;
     const _menu = [...MENU_TEMPLATE];

+ 4 - 0
charge-front/src/pages-user/wallet-recharge/wallet-recharge.vue

@@ -82,6 +82,7 @@ import {ref} from "vue";
 import {fetchProfile, insertMoney} from "../../api/user";
 import {onLoad} from "@dcloudio/uni-app";
 import {back, to} from "@/utils/navigate";
+import { requireAuth } from "@/utils/auth";
 import {debounce} from "@/utils/util";
 import StyleDialog from "@/components/style-dialog/style-dialog.vue";
 
@@ -181,6 +182,9 @@ const confirm = () => {
 
 
 onLoad((options: any) => {
+  if (!requireAuth()) {
+    return;
+  }
   console.log(options)
   if (options.value) {
     payOption.value = payOptions.value.findIndex(

+ 4 - 1
charge-front/src/pages-user/wallet/wallet.vue

@@ -121,6 +121,7 @@ import {fetchWallet, listRefund} from "../../api/user";
 import {to} from "../../utils/navigate";
 import {ref} from "vue";
 import {rpxToPx} from "@/utils/device";
+import { requireAuth } from "@/utils/auth";
 
 const user = ref();
 
@@ -167,7 +168,9 @@ const typeMap = ref(["充值", "退款", "消费"]);
 const scrollViewHeight = ref(0);
 
 onShow(() => {
-  // infiniteScroller.refresh();
+  if (!requireAuth()) {
+    return;
+  }
   loadData();
   if (getApp<any>().globalData.user) {
     user.value = getApp<any>().globalData.user;

+ 6 - 0
charge-front/src/pages.json

@@ -15,6 +15,12 @@
     },
     {
       "path": "pages/index/index"
+    },
+    {
+      "path": "pages/login/login",
+      "style": {
+        "navigationStyle": "default"
+      }
     }
   ],
   "subPackages": [

+ 7 - 2
charge-front/src/pages/list/list.vue

@@ -41,15 +41,20 @@
 import { fetchStations } from "../../api/charge";
 import { fetchCollectList } from "../../api/user";
 import { useInfiniteScroll } from "../../utils/infinite-scroll";
+import { isAuthenticated } from "@/utils/auth";
 import { onLoad, onReachBottom } from "@dcloudio/uni-app";
 const infiniteScroller = useInfiniteScroll(6, (page) => {
   let {latitude, longitude} = getApp<any>().globalData.fetchLocation
   return fetchStations(page, 6,latitude, longitude);
 });
 onLoad(() => {
-  fetchCollectList().then(() => {
+  if (isAuthenticated()) {
+    fetchCollectList().then(() => {
+      infiniteScroller.refresh();
+    });
+  } else {
     infiniteScroller.refresh();
-  });
+  }
 });
 onReachBottom(() => {
   infiniteScroller.next();

+ 131 - 0
charge-front/src/pages/login/login.vue

@@ -0,0 +1,131 @@
+<template>
+  <view class="login-container">
+    <view class="login-content">
+      <image
+        class="logo"
+        src="/static/images/map-logo.png"
+        mode="aspectFit"
+      />
+      <view class="title fs-36 fw-600 mt-40">欢迎使用充电桩</view>
+      <view class="subtitle fs-28 color-666 mt-16">登录后可享受更多功能</view>
+      <view class="btn-area mt-60">
+        <button
+          class="login-btn"
+          open-type="getPhoneNumber"
+          @getphonenumber="handleLogin"
+        >
+          微信手机号一键登录
+        </button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { login } from "@/api/auth";
+import { isAuthenticated, isTabBarPage } from "@/utils/auth";
+import { onLoad } from "@dcloudio/uni-app";
+import { ref } from "vue";
+
+const redirect = ref("");
+
+onLoad((query: any) => {
+  if (isAuthenticated()) {
+    performRedirect(query?.redirect || "");
+    return;
+  }
+  if (query?.redirect) {
+    redirect.value = decodeURIComponent(query.redirect);
+  }
+});
+
+const handleLogin = (e: any) => {
+  login(e)
+    .then(() => {
+      uni.showToast({
+        title: "登录成功",
+        icon: "success",
+        duration: 1000,
+      });
+      setTimeout(() => {
+        performRedirect(redirect.value);
+      }, 1000);
+    })
+    .catch(() => {});
+};
+
+const performRedirect = (url: string) => {
+  if (url && isTabBarPage(url)) {
+    uni.switchTab({ url });
+    return;
+  }
+  if (url) {
+    uni.navigateBack({
+      fail() {
+        uni.switchTab({ url: "/pages/map/map" });
+      },
+    });
+    return;
+  }
+  const pages = getCurrentPages();
+  if (pages.length > 1) {
+    uni.navigateBack({
+      fail() {
+        uni.switchTab({ url: "/pages/map/map" });
+      },
+    });
+  } else {
+    uni.switchTab({ url: "/pages/map/map" });
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.login-container {
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: #fff;
+  padding: 0 60rpx;
+}
+
+.login-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.logo {
+  width: 180rpx;
+  height: 180rpx;
+}
+
+.title {
+  color: #000;
+}
+
+.subtitle {
+  color: #999;
+}
+
+.btn-area {
+  width: 100%;
+}
+
+.login-btn {
+  width: 100%;
+  height: 88rpx;
+  background: var(--color-primary);
+  color: #fff;
+  font-size: 32rpx;
+  border-radius: 44rpx;
+  line-height: 88rpx;
+  border: none;
+}
+
+.login-btn::after {
+  border: none;
+}
+</style>

+ 2 - 22
charge-front/src/pages/map/map.vue

@@ -241,13 +241,6 @@
           <view class="fs-22 mt-14 color-000-5">{{
             loading ? "加载中" : "暂无充电站信息"
           }}</view>
-          <view v-if="!token && !loading" class="mt-32">
-            <button
-              open-type="getPhoneNumber"
-              @getphonenumber="handleLogin"
-              class="login-btn"
-            >登录/注册,查看更多电站</button>
-          </view>
         </view>
       </block>
     </view>
@@ -288,7 +281,7 @@ const pointSize = {
 import { fetchHomeBanner } from "@/api";
 import { deCode } from "../../utils/code";
 import { rpxToPx } from "../../utils/device";
-import { fetchToken, login, onLogin } from "@/api/auth";
+import { fetchToken, onLogin } from "@/api/auth";
 import { fetchStations, fetchChargeStatus } from "@/api/charge";
 import { fetchCollectList } from "@/api/user";
 import { fetchLocation } from "@/utils/location";
@@ -550,7 +543,7 @@ const toSearch = () => {
 
 onLoad((query: any) => {
   // 只为了打包进tab-bar使用
-  console.log(fetchToken, login, onLogin);
+  console.log(fetchToken, onLogin);
   // 扫普通码
   if (query.q) {
     console.log("扫普通码", decodeURIComponent(query.q));
@@ -746,9 +739,6 @@ const tapMarker = (e: any) => {
     _changeMarker(findIndex);
   }
 };
-const handleLogin = (e: any) => {
-  login(e);
-};
 
 let startpageY = 0;
 const touchCardStart = (e: any) => {
@@ -803,16 +793,6 @@ page {
   overflow: hidden;
 }
 
-.login-btn {
-  width: 80vw;
-  height: 80rpx;
-  background: var(--color-primary);
-  color: #fff;
-  font-size: 30rpx;
-  border-radius: 40rpx;
-  line-height: 80rpx;
-}
-
 .dialog {
   position: fixed;
   top: 0;

+ 13 - 21
charge-front/src/pages/user/user.vue

@@ -78,11 +78,7 @@
         <view class="main flex-shrink flex-column flex-align-center pt-40">
           <view class="fs-32 color-000-6 mt-40">登录后查看更多功能</view>
           <view class="mt-32">
-            <button
-              open-type="getPhoneNumber"
-              @getphonenumber="handleLogin"
-              class="login-btn"
-            >登录/注册</button>
+            <button class="login-btn" @click="goToLogin">登录/注册</button>
           </view>
         </view>
       </view>
@@ -93,8 +89,7 @@
 <script setup lang="ts">
 import { fetchContact } from "@/api";
 import { fetchProfile } from "@/api/user";
-import { login } from "@/api/auth";
-import { fetchToken } from "@/api/auth";
+import { isAuthenticated, requireAuth } from "@/utils/auth";
 import { onLoad, onShow } from "@dcloudio/uni-app";
 import { ref } from "vue";
 const containerStyle = ref({});
@@ -144,8 +139,15 @@ const menu = ref([
     icon: "/static/images/user/1.png",
   },
 ]);
+const AUTH_REQUIRED_INDICES = [0, 1, 2, 3, 4];
+
+const goToLogin = () => {
+  requireAuth("/pages/user/user");
+};
+
 const toPage = (index: number) => {
   if (index < 0) {
+    if (!requireAuth("/pages-user/wallet/wallet")) return;
     uni.navigateTo({
       url: "/pages-user/wallet/wallet",
     });
@@ -158,24 +160,14 @@ const toPage = (index: number) => {
     });
     return;
   }
+  if (AUTH_REQUIRED_INDICES.includes(index) && !requireAuth(item.path)) {
+    return;
+  }
   uni.navigateTo({
     url: item.path + (item.title === "常见问题" ? `?service=${service.value}` : ""),
   });
 };
 
-const handleLogin = (e: any) => {
-  login(e).then(() => {
-    fetchProfile().then((res) => {
-      res.mobilePhoneFormat =
-        res.mobilePhone.slice(0, 3) + "****" + res.mobilePhone.slice(7);
-      res.avatar = res.avatar
-        ? res.avatar
-        : "https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0";
-      user.value = res;
-    });
-  });
-};
-
 onLoad(() => {
   const bound = uni.getMenuButtonBoundingClientRect();
   containerStyle.value = {
@@ -188,7 +180,7 @@ onLoad(() => {
   });
 });
 onShow(() => {
-  if (!getApp<any>().globalData.token) {
+  if (!isAuthenticated()) {
     return;
   }
   fetchProfile().then((res) => {

+ 32 - 0
charge-front/src/utils/auth.ts

@@ -0,0 +1,32 @@
+const TAB_BAR_PAGES = ["/pages/map/map", "/pages/user/user"];
+
+export function isAuthenticated(): boolean {
+  return !!getApp<any>().globalData.token;
+}
+
+export function isTabBarPage(path: string): boolean {
+  if (!path) return false;
+  return TAB_BAR_PAGES.some((p) => path.indexOf(p) >= 0);
+}
+
+export function requireAuth(redirectUrl?: string): boolean {
+  if (isAuthenticated()) {
+    return true;
+  }
+
+  const pages = getCurrentPages();
+  const currentPage = pages[pages.length - 1];
+  if (currentPage && currentPage.route === "pages/login/login") {
+    return false;
+  }
+
+  const redirect = redirectUrl
+    ? `/${redirectUrl}`.replace(/^\/+/, "/")
+    : `/${currentPage?.route || "pages/map/map"}`;
+
+  uni.navigateTo({
+    url: `/pages/login/login?redirect=${encodeURIComponent(redirect)}`,
+  });
+
+  return false;
+}