Преглед на файлове

实现用户权限认证

Zhangbw преди 3 месеца
родител
ревизия
45d44b4a95

+ 10 - 0
dist/dev/mp-weixin/app.js

@@ -1,6 +1,8 @@
 "use strict";
 Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
 const common_vendor = require("./common/vendor.js");
+const utils_auth = require("./utils/auth.js");
+require("./utils/api.js");
 if (!Math) {
   "./pages/index/index.js";
   "./pages/login/login.js";
@@ -18,15 +20,23 @@ const _sfc_main = {
   },
   onLaunch: function() {
     console.log("App Launch");
+    if (utils_auth.isLoggedIn()) {
+      console.log("[App] 用户已登录,启动状态检查");
+      utils_auth.startStatusCheck();
+    }
   },
   onShow: function() {
     console.log("App Show");
     common_vendor.index.setNavigationBarTitle({
       title: "量化交易大师"
     });
+    if (utils_auth.isLoggedIn()) {
+      utils_auth.startStatusCheck();
+    }
   },
   onHide: function() {
     console.log("App Hide");
+    utils_auth.stopStatusCheck();
   }
 };
 const App = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["__file", "D:/program/gupiao-wx/src/App.vue"]]);

+ 2 - 2
dist/dev/mp-weixin/pages/admin/shortPool.js

@@ -55,8 +55,8 @@ const _sfc_main = {
         refreshTimer = null;
       }
     };
-    const checkAdminPermission = () => {
-      const userInfo = utils_auth.getUserInfo();
+    const checkAdminPermission = async () => {
+      const userInfo = await utils_auth.refreshUserInfo();
       if (!userInfo || userInfo.status !== 2) {
         common_vendor.index.showToast({ title: "无权限访问", icon: "none" });
         setTimeout(() => common_vendor.index.navigateBack(), 1500);

+ 9 - 3
dist/dev/mp-weixin/pages/mine/mine.js

@@ -18,14 +18,16 @@ const _sfc_main = {
     });
     const loadUserInfo = async () => {
       isLoggedIn.value = utils_auth.isLoggedIn();
-      console.log("[个人中心] 登录状态:", isLoggedIn.value);
       if (isLoggedIn.value) {
         const storedInfo = utils_auth.getUserInfo();
-        console.log("[个人中心] 存储的用户信息:", JSON.stringify(storedInfo));
         if (storedInfo) {
           userInfo.value = storedInfo;
           isAdmin.value = storedInfo.status === 2;
-          console.log("[个人中心] status值:", storedInfo.status, "是否管理员:", isAdmin.value);
+        }
+        const latestInfo = await utils_auth.refreshUserInfo();
+        if (latestInfo) {
+          userInfo.value = latestInfo;
+          isAdmin.value = latestInfo.status === 2;
         }
       } else {
         isAdmin.value = false;
@@ -34,6 +36,10 @@ const _sfc_main = {
     const handleUserCardClick = () => {
       if (!isLoggedIn.value) {
         handleLogin();
+      } else {
+        common_vendor.index.navigateTo({
+          url: "/pages/profile/edit"
+        });
       }
     };
     const handleLogin = async () => {

+ 108 - 80
dist/dev/mp-weixin/pages/profile/edit.js

@@ -3,110 +3,138 @@ const common_vendor = require("../../common/vendor.js");
 const utils_auth = require("../../utils/auth.js");
 const utils_api = require("../../utils/api.js");
 const _sfc_main = {
-  data() {
-    return {
-      avatarUrl: "/static/images/head.png",
-      nickname: "",
-      phone: "",
-      originalAvatar: "",
-      originalNickname: ""
+  __name: "edit",
+  setup(__props) {
+    const avatarUrl = common_vendor.ref("/static/images/head.png");
+    const nickname = common_vendor.ref("");
+    const phone = common_vendor.ref("");
+    const originalAvatar = common_vendor.ref("");
+    const originalNickname = common_vendor.ref("");
+    const saving = common_vendor.ref(false);
+    const handleBack = () => {
+      const pages = getCurrentPages();
+      pages.length > 1 ? common_vendor.index.navigateBack() : common_vendor.index.switchTab({ url: "/pages/mine/mine" });
     };
-  },
-  onLoad() {
-    const loginStatus = utils_auth.isLoggedIn();
-    console.log("[编辑资料] 登录状态:", loginStatus);
-    this.loadUserInfo();
-  },
-  onShow() {
-    common_vendor.index.setNavigationBarTitle({ title: "量化交易大师" });
-  },
-  methods: {
-    /**
-     * 加载用户信息
-     */
-    loadUserInfo() {
+    common_vendor.onMounted(() => {
+      loadUserInfo();
+    });
+    common_vendor.onShow(() => {
+      loadUserInfo();
+    });
+    const loadUserInfo = async () => {
       const userInfo = utils_auth.getUserInfo();
-      console.log("[编辑资料] 加载用户信息:", userInfo);
       if (userInfo) {
-        this.avatarUrl = userInfo.avatar || "/static/images/head.png";
-        this.nickname = userInfo.nickname || "";
-        this.phone = userInfo.phone || "";
-        this.originalAvatar = this.avatarUrl;
-        this.originalNickname = this.nickname;
+        avatarUrl.value = userInfo.avatar || "/static/images/head.png";
+        nickname.value = userInfo.nickname || "";
+        phone.value = userInfo.phone || "";
+        originalAvatar.value = avatarUrl.value;
+        originalNickname.value = nickname.value;
       }
-    },
-    /**
-     * 选择头像
-     */
-    onChooseAvatar(e) {
-      const { avatarUrl } = e.detail;
-      this.avatarUrl = avatarUrl;
-      console.log("选择头像:", avatarUrl);
-    },
-    /**
-     * 保存资料
-     */
-    async handleSave() {
-      if (!this.nickname || this.nickname.trim() === "") {
-        common_vendor.index.showToast({
-          title: "请输入昵称",
-          icon: "none"
+      const latestInfo = await utils_auth.refreshUserInfo();
+      if (latestInfo) {
+        avatarUrl.value = latestInfo.avatar || "/static/images/head.png";
+        nickname.value = latestInfo.nickname || "";
+        phone.value = latestInfo.phone || "";
+        originalAvatar.value = avatarUrl.value;
+        originalNickname.value = nickname.value;
+      }
+    };
+    const formatPhone = (p) => {
+      if (!p || p.length !== 11)
+        return p;
+      return p.substring(0, 3) + "****" + p.substring(7);
+    };
+    const onChooseAvatar = (e) => {
+      avatarUrl.value = e.detail.avatarUrl;
+    };
+    const uploadAvatar = async (tempPath) => {
+      return new Promise((resolve, reject) => {
+        const token = common_vendor.index.getStorageSync("user_token");
+        common_vendor.index.uploadFile({
+          url: "http://localhost:8081/v1/file/upload",
+          filePath: tempPath,
+          name: "file",
+          header: {
+            "Authorization": `Bearer ${token}`
+          },
+          success: (res) => {
+            var _a;
+            if (res.statusCode === 200) {
+              const data = JSON.parse(res.data);
+              if (data.code === 200 && ((_a = data.data) == null ? void 0 : _a.url)) {
+                resolve(data.data.url);
+              } else {
+                reject(new Error(data.message || "上传失败"));
+              }
+            } else {
+              reject(new Error("上传失败"));
+            }
+          },
+          fail: (err) => {
+            reject(new Error("网络错误"));
+          }
         });
+      });
+    };
+    const handleSave = async () => {
+      if (!nickname.value || nickname.value.trim() === "") {
+        common_vendor.index.showToast({ title: "请输入昵称", icon: "none" });
         return;
       }
-      if (this.avatarUrl === this.originalAvatar && this.nickname === this.originalNickname) {
-        common_vendor.index.showToast({
-          title: "没有修改",
-          icon: "none"
-        });
+      if (avatarUrl.value === originalAvatar.value && nickname.value === originalNickname.value) {
+        common_vendor.index.showToast({ title: "没有修改", icon: "none" });
         return;
       }
+      saving.value = true;
+      common_vendor.index.showLoading({ title: "保存中..." });
       try {
-        common_vendor.index.showLoading({ title: "保存中..." });
-        let uploadedAvatarUrl = this.avatarUrl;
-        if (this.avatarUrl !== this.originalAvatar && !this.avatarUrl.startsWith("/static/")) {
-          uploadedAvatarUrl = this.avatarUrl;
+        let uploadedAvatarUrl = avatarUrl.value;
+        if (avatarUrl.value !== originalAvatar.value && !avatarUrl.value.startsWith("/static/") && !avatarUrl.value.startsWith("http")) {
+          try {
+            uploadedAvatarUrl = await uploadAvatar(avatarUrl.value);
+          } catch (e) {
+            console.warn("头像上传失败,使用默认头像:", e.message);
+            uploadedAvatarUrl = "/static/images/head.png";
+          }
         }
-        const result = await utils_api.updateUserProfile({
-          nickname: this.nickname,
+        await utils_api.updateUserProfile({
+          nickname: nickname.value.trim(),
           avatar: uploadedAvatarUrl
         });
-        console.log("更新成功:", result);
         const userInfo = utils_auth.getUserInfo();
-        userInfo.nickname = this.nickname;
+        userInfo.nickname = nickname.value.trim();
         userInfo.avatar = uploadedAvatarUrl;
         utils_auth.setUserInfo(userInfo);
         common_vendor.index.hideLoading();
-        common_vendor.index.showToast({
-          title: "保存成功",
-          icon: "success"
-        });
+        common_vendor.index.showToast({ title: "保存成功", icon: "success" });
         setTimeout(() => {
           common_vendor.index.navigateBack();
         }, 1500);
       } catch (error) {
         console.error("保存失败:", error);
         common_vendor.index.hideLoading();
-        common_vendor.index.showToast({
-          title: error.message || "保存失败",
-          icon: "none"
-        });
+        common_vendor.index.showToast({ title: error.message || "保存失败", icon: "none" });
+      } finally {
+        saving.value = false;
       }
-    }
+    };
+    return (_ctx, _cache) => {
+      return common_vendor.e({
+        a: common_vendor.o(handleBack),
+        b: avatarUrl.value,
+        c: common_vendor.o(onChooseAvatar),
+        d: nickname.value,
+        e: common_vendor.o(($event) => nickname.value = $event.detail.value),
+        f: phone.value
+      }, phone.value ? {
+        g: common_vendor.t(formatPhone(phone.value))
+      } : {}, {
+        h: common_vendor.t(saving.value ? "保存中..." : "保存"),
+        i: common_vendor.o(handleSave),
+        j: saving.value
+      });
+    };
   }
 };
-function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
-  return common_vendor.e({
-    a: $data.avatarUrl,
-    b: common_vendor.o((...args) => $options.onChooseAvatar && $options.onChooseAvatar(...args)),
-    c: $data.nickname,
-    d: common_vendor.o(($event) => $data.nickname = $event.detail.value),
-    e: $data.phone
-  }, $data.phone ? {
-    f: common_vendor.t($data.phone)
-  } : {}, {
-    g: common_vendor.o((...args) => $options.handleSave && $options.handleSave(...args))
-  });
-}
-const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-7e5a80f3"], ["__file", "D:/program/gupiao-wx/src/pages/profile/edit.vue"]]);
+const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["__scopeId", "data-v-7e5a80f3"], ["__file", "D:/program/gupiao-wx/src/pages/profile/edit.vue"]]);
 wx.createPage(MiniProgramPage);

+ 1 - 1
dist/dev/mp-weixin/pages/profile/edit.json

@@ -1,4 +1,4 @@
 {
-  "navigationBarTitleText": "量化交易大师",
+  "navigationStyle": "custom",
   "usingComponents": {}
 }

+ 1 - 1
dist/dev/mp-weixin/pages/profile/edit.wxml

@@ -1 +1 @@
-<view class="edit-profile-container data-v-7e5a80f3"><view class="edit-header data-v-7e5a80f3"><text class="header-title data-v-7e5a80f3">编辑资料</text></view><view class="edit-form data-v-7e5a80f3"><view class="form-item data-v-7e5a80f3"><text class="item-label data-v-7e5a80f3">头像</text><button class="avatar-btn data-v-7e5a80f3" open-type="chooseAvatar" bindchooseavatar="{{b}}"><image class="avatar-preview data-v-7e5a80f3" src="{{a}}" mode="aspectFill"></image><text class="change-text data-v-7e5a80f3">点击更换</text></button></view><view class="form-item data-v-7e5a80f3"><text class="item-label data-v-7e5a80f3">昵称</text><input class="item-input data-v-7e5a80f3" type="nickname" placeholder="请输入昵称" maxlength="20" value="{{c}}" bindinput="{{d}}"/></view><view wx:if="{{e}}" class="form-item data-v-7e5a80f3"><text class="item-label data-v-7e5a80f3">手机号</text><text class="item-value data-v-7e5a80f3">{{f}}</text></view></view><view class="action-area data-v-7e5a80f3"><button class="save-btn data-v-7e5a80f3" bindtap="{{g}}">保存</button></view></view>
+<view class="page-container data-v-7e5a80f3"><view class="custom-navbar data-v-7e5a80f3"><view class="navbar-back data-v-7e5a80f3" bindtap="{{a}}"><text class="back-icon data-v-7e5a80f3">←</text></view><view class="navbar-title data-v-7e5a80f3"><text class="title-text data-v-7e5a80f3">编辑资料</text></view><view class="navbar-placeholder data-v-7e5a80f3"></view></view><scroll-view class="scroll-view data-v-7e5a80f3" scroll-y><view class="content-wrapper data-v-7e5a80f3"><view class="form-card data-v-7e5a80f3"><view class="form-item data-v-7e5a80f3"><text class="item-label data-v-7e5a80f3">头像</text><view class="avatar-section data-v-7e5a80f3"><button class="avatar-btn data-v-7e5a80f3" open-type="chooseAvatar" bindchooseavatar="{{c}}"><image class="avatar-preview data-v-7e5a80f3" src="{{b}}" mode="aspectFill"></image></button><text class="change-tip data-v-7e5a80f3">点击更换</text></view></view><view class="form-item data-v-7e5a80f3"><text class="item-label data-v-7e5a80f3">昵称</text><input class="item-input data-v-7e5a80f3" type="nickname" placeholder="请输入昵称" maxlength="20" value="{{d}}" bindinput="{{e}}"/></view><view wx:if="{{f}}" class="form-item data-v-7e5a80f3"><text class="item-label data-v-7e5a80f3">手机号</text><text class="item-value data-v-7e5a80f3">{{g}}</text></view></view><view class="action-section data-v-7e5a80f3"><button class="save-btn data-v-7e5a80f3" bindtap="{{i}}" disabled="{{j}}">{{h}}</button></view><view class="bottom-safe-area data-v-7e5a80f3"></view></view></scroll-view></view>

+ 87 - 39
dist/dev/mp-weixin/pages/profile/edit.wxss

@@ -1,44 +1,84 @@
 
-.edit-profile-container.data-v-7e5a80f3 {
+.page-container.data-v-7e5a80f3 {
   min-height: 100vh;
   background: #f5f6fb;
-  padding: 30rpx;
+  display: flex;
+  flex-direction: column;
 }
-.edit-header.data-v-7e5a80f3 {
-  text-align: center;
-  padding: 40rpx 0;
+
+/* 导航栏 */
+.custom-navbar.data-v-7e5a80f3 {
+  background: #ffffff;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 80rpx 32rpx 30rpx;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
+  position: relative;
 }
-.header-title.data-v-7e5a80f3 {
+.navbar-back.data-v-7e5a80f3 {
+  width: 80rpx;
+  height: 60rpx;
+  display: flex;
+  align-items: center;
+}
+.back-icon.data-v-7e5a80f3 {
   font-size: 40rpx;
+  color: #222222;
   font-weight: bold;
-  color: #222;
 }
-.edit-form.data-v-7e5a80f3 {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 40rpx;
+.navbar-title.data-v-7e5a80f3 {
+  position: absolute;
+  left: 50%;
+  transform: translateX(-50%);
+}
+.title-text.data-v-7e5a80f3 {
+  font-size: 36rpx;
+  font-weight: 600;
+  color: #222222;
+}
+.navbar-placeholder.data-v-7e5a80f3 {
+  width: 80rpx;
+}
+
+/* 内容区 */
+.scroll-view.data-v-7e5a80f3 {
+  flex: 1;
+  height: 0;
+}
+.content-wrapper.data-v-7e5a80f3 {
+  padding: 32rpx;
+}
+
+/* 表单卡片 */
+.form-card.data-v-7e5a80f3 {
+  background: #ffffff;
+  border-radius: 24rpx;
+  padding: 16rpx 32rpx;
+  box-shadow: 0 8rpx 24rpx rgba(37, 52, 94, 0.08);
   margin-bottom: 40rpx;
 }
 .form-item.data-v-7e5a80f3 {
   display: flex;
   align-items: center;
-  padding: 30rpx 0;
-  border-bottom: 1rpx solid #f0f0f0;
+  justify-content: space-between;
+  padding: 32rpx 0;
+  border-bottom: 1rpx solid #f5f6fb;
 }
 .form-item.data-v-7e5a80f3:last-child {
   border-bottom: none;
 }
 .item-label.data-v-7e5a80f3 {
-  width: 150rpx;
-  font-size: 32rpx;
-  color: #333;
-  font-weight: 500;
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #222222;
+  min-width: 120rpx;
 }
-.avatar-btn.data-v-7e5a80f3 {
-  flex: 1;
+.avatar-section.data-v-7e5a80f3 {
   display: flex;
   align-items: center;
-  justify-content: space-between;
+}
+.avatar-btn.data-v-7e5a80f3 {
   background: transparent;
   border: none;
   padding: 0;
@@ -52,39 +92,47 @@
   width: 120rpx;
   height: 120rpx;
   border-radius: 50%;
-  background: #f0f0f0;
+  background: #f5f6fb;
 }
-.change-text.data-v-7e5a80f3 {
-  font-size: 28rpx;
-  color: #5d55e8;
+.change-tip.data-v-7e5a80f3 {
+  font-size: 26rpx;
+  color: #5B5AEA;
+  margin-left: 20rpx;
 }
 .item-input.data-v-7e5a80f3 {
   flex: 1;
-  height: 80rpx;
-  font-size: 32rpx;
-  color: #333;
+  height: 60rpx;
+  font-size: 30rpx;
+  color: #222222;
   text-align: right;
 }
 .item-value.data-v-7e5a80f3 {
-  flex: 1;
-  font-size: 32rpx;
-  color: #999;
-  text-align: right;
+  font-size: 30rpx;
+  color: #9ca2b5;
 }
-.action-area.data-v-7e5a80f3 {
-  padding: 0 40rpx;
+
+/* 保存按钮 */
+.action-section.data-v-7e5a80f3 {
+  padding: 0 20rpx;
 }
 .save-btn.data-v-7e5a80f3 {
   width: 100%;
-  height: 90rpx;
-  background: linear-gradient(135deg, #5d55e8, #7568ff);
-  color: #fff;
+  height: 96rpx;
+  background: #5B5AEA;
+  color: #ffffff;
   font-size: 32rpx;
-  border-radius: 45rpx;
+  font-weight: 600;
+  border-radius: 48rpx;
   border: none;
-  box-shadow: 0 12rpx 24rpx rgba(93, 85, 232, 0.4);
-  line-height: 90rpx;
+  box-shadow: 0 12rpx 24rpx rgba(91, 90, 234, 0.3);
+  line-height: 96rpx;
 }
 .save-btn.data-v-7e5a80f3:active {
   opacity: 0.9;
 }
+.save-btn[disabled].data-v-7e5a80f3 {
+  opacity: 0.6;
+}
+.bottom-safe-area.data-v-7e5a80f3 {
+  height: 80rpx;
+}

+ 92 - 0
dist/dev/mp-weixin/utils/auth.js

@@ -3,6 +3,9 @@ const common_vendor = require("../common/vendor.js");
 const utils_api = require("./api.js");
 const TOKEN_KEY = "user_token";
 const USER_INFO_KEY = "user_info";
+let statusCheckTimer = null;
+const STATUS_CHECK_INTERVAL = 1e4;
+let isShowingDisabledModal = false;
 const setToken = (token) => {
   common_vendor.index.setStorageSync(TOKEN_KEY, token);
 };
@@ -26,6 +29,7 @@ const isLoggedIn = () => {
   return !!getToken();
 };
 const logout = () => {
+  stopStatusCheck();
   removeToken();
   removeUserInfo();
 };
@@ -38,6 +42,7 @@ const wxSilentLogin = async (loginCode) => {
       setToken(result.data.token);
       console.log("[静默登录] 老用户登录成功");
       await fetchAndSaveUserInfo();
+      startStatusCheck();
     }
     return result.data;
   } catch (error) {
@@ -54,6 +59,7 @@ const wxPhoneLogin = async (params) => {
       setToken(result.data.token);
       console.log("[手机号登录] 已注册用户登录成功");
       await fetchAndSaveUserInfo();
+      startStatusCheck();
     }
     return result.data;
   } catch (error) {
@@ -70,6 +76,7 @@ const wxCompleteUserInfo = async (userInfo) => {
       setToken(result.data.token);
       console.log("[完善信息] 注册成功");
       await fetchAndSaveUserInfo();
+      startStatusCheck();
       return true;
     } else {
       throw new Error(result.message || "注册失败");
@@ -90,6 +97,88 @@ const fetchAndSaveUserInfo = async () => {
     console.error("[用户信息] 获取失败:", error);
   }
 };
+const refreshUserInfo = async () => {
+  if (!isLoggedIn()) {
+    return null;
+  }
+  try {
+    const result = await utils_api.getUserInfoApi();
+    if (result.code === 200 && result.data) {
+      const userInfo = {
+        nickname: result.data.nickname || "",
+        avatar: result.data.avatar || "",
+        phone: result.data.phone || "",
+        status: result.data.status || 0
+      };
+      if (userInfo.status === 1) {
+        console.log("[用户状态] 账号已被禁用,自动退出登录");
+        handleUserDisabled();
+        return null;
+      }
+      setUserInfo(userInfo);
+      console.log("[刷新用户状态] 成功, status:", userInfo.status);
+      return userInfo;
+    }
+  } catch (error) {
+    console.warn("[刷新用户状态] 失败:", error.message);
+  }
+  return getUserInfo();
+};
+const handleUserDisabled = () => {
+  if (isShowingDisabledModal) {
+    return;
+  }
+  isShowingDisabledModal = true;
+  stopStatusCheck();
+  logout();
+  common_vendor.index.showModal({
+    title: "账号已禁用",
+    content: "您的账号已被禁用,如有疑问请联系客服",
+    showCancel: false,
+    confirmText: "我知道了",
+    success: () => {
+      isShowingDisabledModal = false;
+      common_vendor.index.switchTab({ url: "/pages/mine/mine" });
+    }
+  });
+};
+const startStatusCheck = () => {
+  if (statusCheckTimer) {
+    return;
+  }
+  if (!isLoggedIn()) {
+    return;
+  }
+  console.log("[状态检查] 启动定时检查");
+  checkUserStatus();
+  statusCheckTimer = setInterval(() => {
+    checkUserStatus();
+  }, STATUS_CHECK_INTERVAL);
+};
+const stopStatusCheck = () => {
+  if (statusCheckTimer) {
+    console.log("[状态检查] 停止定时检查");
+    clearInterval(statusCheckTimer);
+    statusCheckTimer = null;
+  }
+};
+const checkUserStatus = async () => {
+  if (!isLoggedIn()) {
+    stopStatusCheck();
+    return;
+  }
+  try {
+    const result = await utils_api.getUserInfoApi();
+    if (result.code === 200 && result.data) {
+      if (result.data.status === 1) {
+        console.log("[状态检查] 检测到账号被禁用");
+        handleUserDisabled();
+      }
+    }
+  } catch (error) {
+    console.warn("[状态检查] 检查失败:", error.message);
+  }
+};
 const checkLogin = (callback) => {
   if (isLoggedIn()) {
     return true;
@@ -106,7 +195,10 @@ exports.checkLogin = checkLogin;
 exports.getUserInfo = getUserInfo;
 exports.isLoggedIn = isLoggedIn;
 exports.logout = logout;
+exports.refreshUserInfo = refreshUserInfo;
 exports.setUserInfo = setUserInfo;
+exports.startStatusCheck = startStatusCheck;
+exports.stopStatusCheck = stopStatusCheck;
 exports.wxCompleteUserInfo = wxCompleteUserInfo;
 exports.wxPhoneLogin = wxPhoneLogin;
 exports.wxSilentLogin = wxSilentLogin;

+ 13 - 0
src/App.vue

@@ -1,10 +1,17 @@
 <script>
+import { isLoggedIn, startStatusCheck, stopStatusCheck } from './utils/auth.js'
+
 export default {
   globalData: {
     userInfo: null
   },
   onLaunch: function() {
     console.log('App Launch')
+    // 如果已登录,启动用户状态定时检查
+    if (isLoggedIn()) {
+      console.log('[App] 用户已登录,启动状态检查')
+      startStatusCheck()
+    }
   },
   onShow: function() {
     console.log('App Show')
@@ -12,9 +19,15 @@ export default {
     uni.setNavigationBarTitle({
       title: '量化交易大师'
     })
+    // 应用回到前台时,如果已登录则启动状态检查
+    if (isLoggedIn()) {
+      startStatusCheck()
+    }
   },
   onHide: function() {
     console.log('App Hide')
+    // 应用进入后台时停止状态检查,节省资源
+    stopStatusCheck()
   }
 }
 </script>

+ 0 - 517
src/components/StockListItem.vue

@@ -1,517 +0,0 @@
-<template>
-  <view class="stock-item-wrapper">
-    <!-- 可滑动的内容区域 -->
-    <movable-area class="movable-area">
-      <movable-view 
-        class="movable-view"
-        direction="horizontal"
-        :x="moveX"
-        :damping="40"
-        :friction="5"
-        :out-of-bounds="false"
-        @change="handleMoveChange"
-        @touchend="handleMoveEnd"
-      >
-        <view class="stock-list-item">
-          <!-- 左侧:股票信息 -->
-          <view class="stock-left">
-            <view class="stock-name-row">
-              <text class="stock-name">{{ stock.name }}</text>
-              <text :class="['stock-tag', getMarketClass(stock.code)]">{{ getMarketTag(stock.code) }}</text>
-            </view>
-            <text class="stock-code">{{ stock.code }}</text>
-          </view>
-
-          <!-- 中间:涨跌趋势图 -->
-          <view class="stock-chart">
-            <canvas 
-              :canvas-id="canvasId" 
-              :id="canvasId"
-              class="trend-canvas"
-            ></canvas>
-          </view>
-
-          <!-- 右侧:涨跌幅和价格 -->
-          <view class="stock-right">
-            <view 
-              v-if="hasValidChange(stock.changePercent)" 
-              :class="['change-percent', getChangeClass(stock.changePercent)]"
-            >
-              {{ formatChangePercent(stock.changePercent) }}
-            </view>
-            <text class="stock-price">{{ formatPrice(stock.currentPrice) }}</text>
-          </view>
-          
-          <!-- 删除按钮(在内容右侧) -->
-          <view v-if="showDelete" class="delete-action" @click.stop="handleDelete">
-            <view class="delete-icon-wrapper">
-              <text class="delete-icon">−</text>
-            </view>
-          </view>
-        </view>
-      </movable-view>
-    </movable-area>
-  </view>
-</template>
-
-<script setup>
-import { onMounted, onUnmounted, nextTick, getCurrentInstance, ref, watch } from 'vue'
-
-const props = defineProps({
-  stock: {
-    type: Object,
-    required: true
-  },
-  showDelete: {
-    type: Boolean,
-    default: false
-  }
-})
-
-const emit = defineEmits(['delete'])
-
-// 保存组件实例引用
-let componentInstance = null
-
-// 滑动删除相关
-const deleteWidth = 60 // 删除按钮宽度(px)
-const moveX = ref(0)
-let currentX = 0
-let autoResetTimer = null // 自动还原计时器
-let checkTimer = null // 检查计时器(备用)
-const AUTO_RESET_DELAY = 2500 // 自动还原延迟时间(ms)
-let isSlideOpen = false // 标记滑动是否打开
-
-// 强制还原到初始位置
-const forceReset = () => {
-  console.log('[滑动删除] 强制还原')
-  moveX.value = 0
-  currentX = 0
-  isSlideOpen = false
-  clearAutoResetTimer()
-}
-
-// 启动自动还原计时器
-const startAutoResetTimer = () => {
-  // 清除之前的计时器
-  clearAutoResetTimer()
-  
-  isSlideOpen = true
-  console.log('[滑动删除] 启动自动还原计时器')
-  
-  // 主计时器
-  autoResetTimer = setTimeout(() => {
-    console.log('[滑动删除] 主计时器触发')
-    forceReset()
-  }, AUTO_RESET_DELAY)
-  
-  // 备用检查计时器,每500ms检查一次,确保一定会还原
-  checkTimer = setInterval(() => {
-    if (isSlideOpen && moveX.value < 0) {
-      // 如果主计时器失效,备用计时器在3秒后强制还原
-      console.log('[滑动删除] 备用检查中...')
-    } else if (isSlideOpen && moveX.value === 0) {
-      // 已经还原了,清理状态
-      isSlideOpen = false
-      clearAutoResetTimer()
-    }
-  }, 500)
-  
-  // 额外的保险:3.5秒后无论如何都还原
-  setTimeout(() => {
-    if (isSlideOpen) {
-      console.log('[滑动删除] 保险计时器触发')
-      forceReset()
-    }
-  }, AUTO_RESET_DELAY + 1000)
-}
-
-// 清除自动还原计时器
-const clearAutoResetTimer = () => {
-  if (autoResetTimer) {
-    clearTimeout(autoResetTimer)
-    autoResetTimer = null
-  }
-  if (checkTimer) {
-    clearInterval(checkTimer)
-    checkTimer = null
-  }
-}
-
-// 生成稳定的 canvas ID(只在组件创建时生成一次)
-const canvasId = ref(`chart-${props.stock.code}-${Math.random().toString(36).slice(2, 11)}`)
-
-// 判断市场类型(沪市/深市/创业板)
-const getMarketTag = (code) => {
-  if (code.startsWith('6')) return '沪'
-  if (code.startsWith('0')) return '深'
-  if (code.startsWith('3')) return '创'
-  return '沪'
-}
-
-const getMarketClass = (code) => {
-  if (code.startsWith('6')) return 'market-sh'
-  if (code.startsWith('0')) return 'market-sz'
-  if (code.startsWith('3')) return 'market-cy'
-  return 'market-sh'
-}
-
-// 获取涨跌样式类
-const getChangeClass = (changePercent) => {
-  if (!changePercent) return ''
-  const str = String(changePercent).replace('%', '').replace('+', '')
-  const value = parseFloat(str)
-  if (value > 0) return 'change-up'
-  if (value < 0) return 'change-down'
-  return ''
-}
-
-// 格式化涨跌幅
-const formatChangePercent = (changePercent) => {
-  if (!changePercent) return '--'
-  return String(changePercent)
-}
-
-// 格式化价格
-const formatPrice = (price) => {
-  if (!price) return '--'
-  return parseFloat(price).toFixed(2)
-}
-
-// 判断是否有有效的涨跌幅(非0、非空)
-const hasValidChange = (changePercent) => {
-  if (!changePercent) return false
-  const str = String(changePercent).replace('%', '').replace('+', '')
-  const value = parseFloat(str)
-  return value !== 0 && !isNaN(value)
-}
-
-// 绘制趋势图
-const drawTrendChart = (instance) => {
-  // 获取趋势数据
-  let trendData = props.stock.trendData
-  
-  // 如果没有趋势数据,生成模拟数据
-  if (!trendData || !Array.isArray(trendData) || trendData.length === 0) {
-    trendData = generateMockTrendData()
-  }
-  
-  // 使用 uni.createCanvasContext 创建画布上下文
-  // 在 setup 中需要传入组件实例
-  const ctx = uni.createCanvasContext(canvasId.value, instance)
-  
-  // 画布尺寸
-  const width = 100  // 实际像素
-  const height = 30  // 实际像素
-  const padding = 2
-  
-  // 计算数据范围
-  const maxValue = Math.max(...trendData)
-  const minValue = Math.min(...trendData)
-  const dataRange = maxValue - minValue
-  // 设置最小范围为数据均值的3%,确保图表有明显起伏
-  const avgValue = (maxValue + minValue) / 2
-  const minRange = avgValue * 0.03 || 1
-  const range = Math.max(dataRange, minRange)
-  
-  // 基准线位置(第一个数据点的值作为基准)
-  const baseValue = trendData[0]
-  const baseY = height - padding - ((baseValue - minValue) / range) * (height - padding * 2)
-  
-  // 判断涨跌颜色
-  const changePercent = parseFloat(String(props.stock.changePercent || '0').replace('%', '').replace('+', ''))
-  const isUp = changePercent >= 0
-  const lineColor = isUp ? '#FF3B30' : '#34C759'  // 红涨绿跌
-  const fillColor = isUp ? 'rgba(255, 59, 48, 0.15)' : 'rgba(52, 199, 89, 0.15)'
-  
-  // 先绘制基准虚线
-  ctx.beginPath()
-  ctx.setStrokeStyle('#e0e0e0')
-  ctx.setLineWidth(0.5)
-  ctx.setLineDash([2, 2], 0)
-  ctx.moveTo(padding, baseY)
-  ctx.lineTo(width - padding, baseY)
-  ctx.stroke()
-  ctx.setLineDash([], 0)  // 重置虚线
-  
-  // 绘制填充区域(从基准线到折线)
-  ctx.beginPath()
-  ctx.moveTo(padding, baseY)
-  
-  trendData.forEach((value, index) => {
-    const x = padding + (index / (trendData.length - 1)) * (width - padding * 2)
-    const y = height - padding - ((value - minValue) / range) * (height - padding * 2)
-    ctx.lineTo(x, y)
-  })
-  
-  ctx.lineTo(width - padding, baseY)
-  ctx.closePath()
-  ctx.setFillStyle(fillColor)
-  ctx.fill()
-  
-  // 绘制折线
-  ctx.beginPath()
-  
-  trendData.forEach((value, index) => {
-    const x = padding + (index / (trendData.length - 1)) * (width - padding * 2)
-    const y = height - padding - ((value - minValue) / range) * (height - padding * 2)
-    
-    if (index === 0) {
-      ctx.moveTo(x, y)
-    } else {
-      ctx.lineTo(x, y)
-    }
-  })
-  
-  ctx.setStrokeStyle(lineColor)
-  ctx.setLineWidth(1.5)
-  ctx.stroke()
-  
-  // 绘制到画布
-  ctx.draw()
-}
-
-// 生成模拟趋势数据
-const generateMockTrendData = () => {
-  const changePercent = parseFloat(String(props.stock.changePercent || '0').replace('%', '').replace('+', ''))
-  const points = 15  // 数据点数
-  const data = []
-  
-  // 基于涨跌幅生成趋势数据
-  let baseValue = 100
-  const trend = changePercent / 100  // 总体趋势
-  
-  for (let i = 0; i < points; i++) {
-    // 增大随机波动幅度,让图表更直观
-    const randomChange = (Math.random() - 0.5) * 6
-    const trendChange = (i / points) * trend * 100
-    baseValue = baseValue + randomChange + trendChange / points
-    data.push(baseValue)
-  }
-  
-  return data
-}
-
-// 滑动变化
-const handleMoveChange = (e) => {
-  currentX = e.detail.x
-}
-
-// 滑动结束
-const handleMoveEnd = () => {
-  if (!props.showDelete) return
-  
-  // 滑动超过三分之一就显示删除按钮
-  if (currentX < -deleteWidth / 3) {
-    moveX.value = -deleteWidth
-    // 启动自动还原计时器
-    startAutoResetTimer()
-  } else {
-    // 滑回初始位置
-    forceReset()
-  }
-}
-
-// 处理删除
-const handleDelete = () => {
-  forceReset() // 点击删除时还原位置
-  emit('delete')
-}
-
-// 组件挂载后绘制图表
-onMounted(() => {
-  componentInstance = getCurrentInstance()
-  nextTick(() => {
-    // 延迟绘制,确保 canvas 已渲染
-    setTimeout(() => {
-      drawTrendChart(componentInstance)
-    }, 300)
-  })
-})
-
-// 监听股票数据变化,重新绘制趋势图
-watch(() => props.stock.trendData, (newData) => {
-  if (newData && componentInstance) {
-    nextTick(() => {
-      drawTrendChart(componentInstance)
-    })
-  }
-}, { deep: true })
-
-// 监听涨跌幅变化,更新颜色
-watch(() => props.stock.changePercent, () => {
-  if (componentInstance) {
-    nextTick(() => {
-      drawTrendChart(componentInstance)
-    })
-  }
-})
-
-// 监听滑动位置变化
-watch(moveX, (newVal) => {
-  if (newVal === 0 && isSlideOpen) {
-    // 已还原,清理状态
-    isSlideOpen = false
-    clearAutoResetTimer()
-  }
-})
-
-// 组件销毁时清理计时器
-onUnmounted(() => {
-  clearAutoResetTimer()
-})
-</script>
-
-<style scoped>
-.stock-item-wrapper {
-  position: relative;
-  height: 120rpx;
-  overflow: hidden;
-  background: #ffffff;
-}
-
-.movable-area {
-  width: 100%;
-  height: 100%;
-}
-
-.movable-view {
-  width: calc(100% + 120rpx);
-  height: 100%;
-}
-
-.stock-list-item {
-  display: flex;
-  align-items: center;
-  padding: 24rpx 0;
-  height: 100%;
-  box-sizing: border-box;
-  background: #ffffff;
-  border-bottom: 1rpx solid #f1f2f6;
-}
-
-.stock-item-wrapper:last-child .stock-list-item {
-  border-bottom: none;
-}
-
-/* 滑动删除按钮 */
-.delete-action {
-  flex-shrink: 0;
-  width: 120rpx;
-  height: 100%;
-  background: #ffffff;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.delete-icon-wrapper {
-  width: 64rpx;
-  height: 64rpx;
-  background: #FF3B30;
-  border-radius: 50%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  box-shadow: 0 4rpx 12rpx rgba(255, 59, 48, 0.3);
-}
-
-.delete-icon {
-  font-size: 32rpx;
-  color: #ffffff;
-  font-weight: bold;
-}
-
-/* 左侧股票信息 */
-.stock-left {
-  flex-shrink: 0;
-  width: 160rpx;
-  display: flex;
-  flex-direction: column;
-}
-
-.stock-name-row {
-  display: flex;
-  align-items: center;
-  margin-bottom: 8rpx;
-}
-
-.stock-name {
-  font-size: 26rpx;
-  font-weight: 600;
-  color: #222222;
-  margin-right: 8rpx;
-}
-
-.stock-tag {
-  font-size: 18rpx;
-  padding: 2rpx 6rpx;
-  border-radius: 4rpx;
-  color: #ffffff;
-  font-weight: 500;
-}
-
-.market-sh {
-  background: #FF3B30;
-}
-
-.market-sz {
-  background: #34C759;
-}
-
-.market-cy {
-  background: #FF9500;
-}
-
-.stock-code {
-  font-size: 22rpx;
-  color: #9ca2b5;
-}
-
-/* 中间趋势图 */
-.stock-chart {
-  flex: 1;
-  height: 60rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  margin: 0 16rpx;
-}
-
-.trend-canvas {
-  width: 200rpx;
-  height: 60rpx;
-}
-
-/* 右侧涨跌幅和价格 */
-.stock-right {
-  flex-shrink: 0;
-  width: 120rpx;
-  display: flex;
-  flex-direction: column;
-  align-items: flex-end;
-}
-
-.change-percent {
-  font-size: 26rpx;
-  font-weight: 700;
-  padding: 4rpx 12rpx;
-  border-radius: 6rpx;
-  margin-bottom: 8rpx;
-}
-
-.change-up {
-  background: #FF3B30;
-  color: #ffffff;
-}
-
-.change-down {
-  background: #34C759;
-  color: #ffffff;
-}
-
-.stock-price {
-  font-size: 22rpx;
-  color: #666a7f;
-}
-</style>

+ 1 - 1
src/pages.json

@@ -39,7 +39,7 @@
     {
       "path": "pages/profile/edit",
       "style": {
-        "navigationBarTitleText": "量化交易大师"
+        "navigationStyle": "custom"
       }
     },
     {

+ 4 - 3
src/pages/admin/shortPool.vue

@@ -120,7 +120,7 @@
 <script setup>
 import { ref, onUnmounted } from 'vue'
 import { onShow, onHide } from '@dcloudio/uni-app'
-import { getUserInfo } from '@/utils/auth.js'
+import { refreshUserInfo } from '@/utils/auth.js'
 import { getSuggestions } from '@/utils/api.js'
 
 const searchKeyword = ref('')
@@ -179,8 +179,9 @@ const stopAutoRefresh = () => {
   }
 }
 
-const checkAdminPermission = () => {
-  const userInfo = getUserInfo()
+// 检查管理员权限(从后端获取最新状态)
+const checkAdminPermission = async () => {
+  const userInfo = await refreshUserInfo()
   if (!userInfo || userInfo.status !== 2) {
     uni.showToast({ title: '无权限访问', icon: 'none' })
     setTimeout(() => uni.navigateBack(), 1500)

+ 16 - 7
src/pages/mine/mine.vue

@@ -54,7 +54,7 @@
 <script setup>
 import { ref } from 'vue'
 import { onShow } from '@dcloudio/uni-app'
-import { isLoggedIn as checkLogin, getUserInfo as getStoredUserInfo, logout, checkLogin as requireLogin } from '@/utils/auth.js'
+import { isLoggedIn as checkLogin, getUserInfo as getStoredUserInfo, refreshUserInfo, logout, checkLogin as requireLogin } from '@/utils/auth.js'
 
 const isLoggedIn = ref(false)
 const isAdmin = ref(false)
@@ -69,24 +69,28 @@ const userInfo = ref({
  */
 onShow(() => {
   loadUserInfo()
-  // 设置导航栏标题
   uni.setNavigationBarTitle({ title: '量化交易大师' })
 })
 
 /**
- * 加载用户信息
+ * 加载用户信息(从后端获取最新状态)
  */
 const loadUserInfo = async () => {
   isLoggedIn.value = checkLogin()
-  console.log('[个人中心] 登录状态:', isLoggedIn.value)
+  
   if (isLoggedIn.value) {
+    // 先显示本地缓存
     const storedInfo = getStoredUserInfo()
-    console.log('[个人中心] 存储的用户信息:', JSON.stringify(storedInfo))
     if (storedInfo) {
       userInfo.value = storedInfo
-      // 判断是否为管理员(status === 2)
       isAdmin.value = storedInfo.status === 2
-      console.log('[个人中心] status值:', storedInfo.status, '是否管理员:', isAdmin.value)
+    }
+    
+    // 从后端刷新最新状态
+    const latestInfo = await refreshUserInfo()
+    if (latestInfo) {
+      userInfo.value = latestInfo
+      isAdmin.value = latestInfo.status === 2
     }
   } else {
     isAdmin.value = false
@@ -99,6 +103,11 @@ const loadUserInfo = async () => {
 const handleUserCardClick = () => {
   if (!isLoggedIn.value) {
     handleLogin()
+  } else {
+    // 已登录,跳转到编辑资料页面
+    uni.navigateTo({
+      url: '/pages/profile/edit'
+    })
   }
 }
 

+ 275 - 180
src/pages/profile/edit.vue

@@ -1,195 +1,280 @@
 <template>
-  <view class="edit-profile-container">
-    <view class="edit-header">
-      <text class="header-title">编辑资料</text>
-    </view>
-
-    <view class="edit-form">
-      <!-- 头像 -->
-      <view class="form-item">
-        <text class="item-label">头像</text>
-        <button class="avatar-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
-          <image class="avatar-preview" :src="avatarUrl" mode="aspectFill"></image>
-          <text class="change-text">点击更换</text>
-        </button>
+  <view class="page-container">
+    <!-- 顶部导航栏 -->
+    <view class="custom-navbar">
+      <view class="navbar-back" @click="handleBack">
+        <text class="back-icon">←</text>
       </view>
-
-      <!-- 昵称 -->
-      <view class="form-item">
-        <text class="item-label">昵称</text>
-        <input 
-          class="item-input" 
-          type="nickname" 
-          v-model="nickname" 
-          placeholder="请输入昵称"
-          maxlength="20"
-        />
-      </view>
-
-      <!-- 手机号(只读) -->
-      <view class="form-item" v-if="phone">
-        <text class="item-label">手机号</text>
-        <text class="item-value">{{ phone }}</text>
+      <view class="navbar-title">
+        <text class="title-text">编辑资料</text>
       </view>
+      <view class="navbar-placeholder"></view>
     </view>
 
-    <!-- 保存按钮 -->
-    <view class="action-area">
-      <button class="save-btn" @click="handleSave">保存</button>
-    </view>
+    <scroll-view class="scroll-view" scroll-y>
+      <view class="content-wrapper">
+        <!-- 表单卡片 -->
+        <view class="form-card">
+          <!-- 头像 -->
+          <view class="form-item">
+            <text class="item-label">头像</text>
+            <view class="avatar-section">
+              <button class="avatar-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
+                <image class="avatar-preview" :src="avatarUrl" mode="aspectFill"></image>
+              </button>
+              <text class="change-tip">点击更换</text>
+            </view>
+          </view>
+
+          <!-- 昵称 -->
+          <view class="form-item">
+            <text class="item-label">昵称</text>
+            <input 
+              class="item-input" 
+              type="nickname" 
+              v-model="nickname" 
+              placeholder="请输入昵称"
+              maxlength="20"
+            />
+          </view>
+
+          <!-- 手机号(只读) -->
+          <view class="form-item" v-if="phone">
+            <text class="item-label">手机号</text>
+            <text class="item-value">{{ formatPhone(phone) }}</text>
+          </view>
+        </view>
+
+        <!-- 保存按钮 -->
+        <view class="action-section">
+          <button class="save-btn" @click="handleSave" :disabled="saving">
+            {{ saving ? '保存中...' : '保存' }}
+          </button>
+        </view>
+
+        <view class="bottom-safe-area"></view>
+      </view>
+    </scroll-view>
   </view>
 </template>
 
-<script>
-import { getUserInfo, setUserInfo, isLoggedIn as checkLoginStatus } from '@/utils/auth.js'
+<script setup>
+import { ref, onMounted } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
+import { getUserInfo, setUserInfo, refreshUserInfo } from '@/utils/auth.js'
 import { updateUserProfile } from '@/utils/api.js'
 
-export default {
-  data() {
-    return {
-      avatarUrl: '/static/images/head.png',
-      nickname: '',
-      phone: '',
-      originalAvatar: '',
-      originalNickname: ''
-    }
-  },
-
-  onLoad() {
-    const loginStatus = checkLoginStatus()
-    console.log('[编辑资料] 登录状态:', loginStatus)
-    this.loadUserInfo()
-  },
-
-  onShow() {
-    // 设置导航栏标题
-    uni.setNavigationBarTitle({ title: '量化交易大师' })
-  },
-
-  methods: {
-    /**
-     * 加载用户信息
-     */
-    loadUserInfo() {
-      const userInfo = getUserInfo()
-      console.log('[编辑资料] 加载用户信息:', userInfo)
-      if (userInfo) {
-        this.avatarUrl = userInfo.avatar || '/static/images/head.png'
-        this.nickname = userInfo.nickname || ''
-        this.phone = userInfo.phone || ''
-        this.originalAvatar = this.avatarUrl
-        this.originalNickname = this.nickname
-      }
-    },
-
-    /**
-     * 选择头像
-     */
-    onChooseAvatar(e) {
-      const { avatarUrl } = e.detail
-      this.avatarUrl = avatarUrl
-      console.log('选择头像:', avatarUrl)
-    },
-
-    /**
-     * 保存资料
-     */
-    async handleSave() {
-      if (!this.nickname || this.nickname.trim() === '') {
-        uni.showToast({
-          title: '请输入昵称',
-          icon: 'none'
-        })
-        return
-      }
+const avatarUrl = ref('/static/images/head.png')
+const nickname = ref('')
+const phone = ref('')
+const originalAvatar = ref('')
+const originalNickname = ref('')
+const saving = ref(false)
 
-      // 检查是否有修改
-      if (this.avatarUrl === this.originalAvatar && this.nickname === this.originalNickname) {
-        uni.showToast({
-          title: '没有修改',
-          icon: 'none'
-        })
-        return
-      }
+const handleBack = () => {
+  const pages = getCurrentPages()
+  pages.length > 1 ? uni.navigateBack() : uni.switchTab({ url: '/pages/mine/mine' })
+}
 
-      try {
-        uni.showLoading({ title: '保存中...' })
+onMounted(() => {
+  loadUserInfo()
+})
+
+onShow(() => {
+  loadUserInfo()
+})
+
+const loadUserInfo = async () => {
+  // 先显示本地缓存
+  const userInfo = getUserInfo()
+  if (userInfo) {
+    avatarUrl.value = userInfo.avatar || '/static/images/head.png'
+    nickname.value = userInfo.nickname || ''
+    phone.value = userInfo.phone || ''
+    originalAvatar.value = avatarUrl.value
+    originalNickname.value = nickname.value
+  }
+  
+  // 刷新最新状态
+  const latestInfo = await refreshUserInfo()
+  if (latestInfo) {
+    avatarUrl.value = latestInfo.avatar || '/static/images/head.png'
+    nickname.value = latestInfo.nickname || ''
+    phone.value = latestInfo.phone || ''
+    originalAvatar.value = avatarUrl.value
+    originalNickname.value = nickname.value
+  }
+}
+
+const formatPhone = (p) => {
+  if (!p || p.length !== 11) return p
+  return p.substring(0, 3) + '****' + p.substring(7)
+}
+
+const onChooseAvatar = (e) => {
+  avatarUrl.value = e.detail.avatarUrl
+}
 
-        // 上传头像(如果更换了头像且不是默认头像)
-        let uploadedAvatarUrl = this.avatarUrl
-        if (this.avatarUrl !== this.originalAvatar && !this.avatarUrl.startsWith('/static/')) {
-          // 暂时直接使用微信临时路径,后续可扩展上传到服务器
-          uploadedAvatarUrl = this.avatarUrl
+const uploadAvatar = async (tempPath) => {
+  return new Promise((resolve, reject) => {
+    const token = uni.getStorageSync('user_token')
+    uni.uploadFile({
+      url: 'http://localhost:8081/v1/file/upload',
+      filePath: tempPath,
+      name: 'file',
+      header: {
+        'Authorization': `Bearer ${token}`
+      },
+      success: (res) => {
+        if (res.statusCode === 200) {
+          const data = JSON.parse(res.data)
+          if (data.code === 200 && data.data?.url) {
+            resolve(data.data.url)
+          } else {
+            reject(new Error(data.message || '上传失败'))
+          }
+        } else {
+          reject(new Error('上传失败'))
         }
+      },
+      fail: (err) => {
+        reject(new Error('网络错误'))
+      }
+    })
+  })
+}
+
+const handleSave = async () => {
+  if (!nickname.value || nickname.value.trim() === '') {
+    uni.showToast({ title: '请输入昵称', icon: 'none' })
+    return
+  }
+
+  if (avatarUrl.value === originalAvatar.value && nickname.value === originalNickname.value) {
+    uni.showToast({ title: '没有修改', icon: 'none' })
+    return
+  }
 
-        // 调用后端接口更新用户信息
-        const result = await updateUserProfile({
-          nickname: this.nickname,
-          avatar: uploadedAvatarUrl
-        })
-
-        console.log('更新成功:', result)
-
-        // 更新本地存储的用户信息
-        const userInfo = getUserInfo()
-        userInfo.nickname = this.nickname
-        userInfo.avatar = uploadedAvatarUrl
-        setUserInfo(userInfo)
-
-        uni.hideLoading()
-        uni.showToast({
-          title: '保存成功',
-          icon: 'success'
-        })
-
-        // 延迟返回
-        setTimeout(() => {
-          uni.navigateBack()
-        }, 1500)
-
-      } catch (error) {
-        console.error('保存失败:', error)
-        uni.hideLoading()
-        uni.showToast({
-          title: error.message || '保存失败',
-          icon: 'none'
-        })
+  saving.value = true
+  uni.showLoading({ title: '保存中...' })
+
+  try {
+    let uploadedAvatarUrl = avatarUrl.value
+
+    // 如果头像是临时文件路径,需要上传到OSS
+    if (avatarUrl.value !== originalAvatar.value && 
+        !avatarUrl.value.startsWith('/static/') && 
+        !avatarUrl.value.startsWith('http')) {
+      try {
+        uploadedAvatarUrl = await uploadAvatar(avatarUrl.value)
+      } catch (e) {
+        console.warn('头像上传失败,使用默认头像:', e.message)
+        uploadedAvatarUrl = '/static/images/head.png'
       }
     }
+
+    // 调用后端接口更新用户信息
+    await updateUserProfile({
+      nickname: nickname.value.trim(),
+      avatar: uploadedAvatarUrl
+    })
+
+    // 更新本地存储
+    const userInfo = getUserInfo()
+    userInfo.nickname = nickname.value.trim()
+    userInfo.avatar = uploadedAvatarUrl
+    setUserInfo(userInfo)
+
+    uni.hideLoading()
+    uni.showToast({ title: '保存成功', icon: 'success' })
+
+    setTimeout(() => {
+      uni.navigateBack()
+    }, 1500)
+
+  } catch (error) {
+    console.error('保存失败:', error)
+    uni.hideLoading()
+    uni.showToast({ title: error.message || '保存失败', icon: 'none' })
+  } finally {
+    saving.value = false
   }
 }
 </script>
 
 <style scoped>
-.edit-profile-container {
+.page-container {
   min-height: 100vh;
   background: #f5f6fb;
-  padding: 30rpx;
+  display: flex;
+  flex-direction: column;
+}
+
+/* 导航栏 */
+.custom-navbar {
+  background: #ffffff;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 80rpx 32rpx 30rpx;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
+  position: relative;
 }
 
-.edit-header {
-  text-align: center;
-  padding: 40rpx 0;
+.navbar-back {
+  width: 80rpx;
+  height: 60rpx;
+  display: flex;
+  align-items: center;
 }
 
-.header-title {
+.back-icon {
   font-size: 40rpx;
+  color: #222222;
   font-weight: bold;
-  color: #222;
 }
 
-.edit-form {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 40rpx;
+.navbar-title {
+  position: absolute;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
+.title-text {
+  font-size: 36rpx;
+  font-weight: 600;
+  color: #222222;
+}
+
+.navbar-placeholder {
+  width: 80rpx;
+}
+
+/* 内容区 */
+.scroll-view {
+  flex: 1;
+  height: 0;
+}
+
+.content-wrapper {
+  padding: 32rpx;
+}
+
+/* 表单卡片 */
+.form-card {
+  background: #ffffff;
+  border-radius: 24rpx;
+  padding: 16rpx 32rpx;
+  box-shadow: 0 8rpx 24rpx rgba(37, 52, 94, 0.08);
   margin-bottom: 40rpx;
 }
 
 .form-item {
   display: flex;
   align-items: center;
-  padding: 30rpx 0;
-  border-bottom: 1rpx solid #f0f0f0;
+  justify-content: space-between;
+  padding: 32rpx 0;
+  border-bottom: 1rpx solid #f5f6fb;
 }
 
 .form-item:last-child {
@@ -197,17 +282,18 @@ export default {
 }
 
 .item-label {
-  width: 150rpx;
-  font-size: 32rpx;
-  color: #333;
-  font-weight: 500;
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #222222;
+  min-width: 120rpx;
 }
 
-.avatar-btn {
-  flex: 1;
+.avatar-section {
   display: flex;
   align-items: center;
-  justify-content: space-between;
+}
+
+.avatar-btn {
   background: transparent;
   border: none;
   padding: 0;
@@ -223,46 +309,55 @@ export default {
   width: 120rpx;
   height: 120rpx;
   border-radius: 50%;
-  background: #f0f0f0;
+  background: #f5f6fb;
 }
 
-.change-text {
-  font-size: 28rpx;
-  color: #5d55e8;
+.change-tip {
+  font-size: 26rpx;
+  color: #5B5AEA;
+  margin-left: 20rpx;
 }
 
 .item-input {
   flex: 1;
-  height: 80rpx;
-  font-size: 32rpx;
-  color: #333;
+  height: 60rpx;
+  font-size: 30rpx;
+  color: #222222;
   text-align: right;
 }
 
 .item-value {
-  flex: 1;
-  font-size: 32rpx;
-  color: #999;
-  text-align: right;
+  font-size: 30rpx;
+  color: #9ca2b5;
 }
 
-.action-area {
-  padding: 0 40rpx;
+/* 保存按钮 */
+.action-section {
+  padding: 0 20rpx;
 }
 
 .save-btn {
   width: 100%;
-  height: 90rpx;
-  background: linear-gradient(135deg, #5d55e8, #7568ff);
-  color: #fff;
+  height: 96rpx;
+  background: #5B5AEA;
+  color: #ffffff;
   font-size: 32rpx;
-  border-radius: 45rpx;
+  font-weight: 600;
+  border-radius: 48rpx;
   border: none;
-  box-shadow: 0 12rpx 24rpx rgba(93, 85, 232, 0.4);
-  line-height: 90rpx;
+  box-shadow: 0 12rpx 24rpx rgba(91, 90, 234, 0.3);
+  line-height: 96rpx;
 }
 
 .save-btn:active {
   opacity: 0.9;
 }
+
+.save-btn[disabled] {
+  opacity: 0.6;
+}
+
+.bottom-safe-area {
+  height: 80rpx;
+}
 </style>

+ 136 - 0
src/utils/auth.js

@@ -20,6 +20,11 @@ import {
 const TOKEN_KEY = 'user_token'
 const USER_INFO_KEY = 'user_info'
 
+// 全局状态检查定时器
+let statusCheckTimer = null
+const STATUS_CHECK_INTERVAL = 10000 // 10秒检查一次
+let isShowingDisabledModal = false // 防止重复弹窗
+
 /**
  * 保存用户token到本地存储
  * @param {string} token - 用户登录token
@@ -79,6 +84,7 @@ export const isLoggedIn = () => {
  * 清除所有登录信息(退出登录)
  */
 export const logout = () => {
+  stopStatusCheck() // 停止状态检查
   removeToken()
   removeUserInfo()
 }
@@ -105,6 +111,9 @@ export const wxSilentLogin = async (loginCode) => {
       
       // 获取用户信息
       await fetchAndSaveUserInfo()
+      
+      // 启动状态检查
+      startStatusCheck()
     }
     
     return result.data
@@ -136,6 +145,9 @@ export const wxPhoneLogin = async (params) => {
       
       // 获取用户信息
       await fetchAndSaveUserInfo()
+      
+      // 启动状态检查
+      startStatusCheck()
     }
     
     return result.data
@@ -167,6 +179,9 @@ export const wxCompleteUserInfo = async (userInfo) => {
       // 获取用户信息
       await fetchAndSaveUserInfo()
       
+      // 启动状态检查
+      startStatusCheck()
+      
       // 记录登录日志(可选)
       // await recordLoginLog()
       
@@ -196,6 +211,127 @@ const fetchAndSaveUserInfo = async () => {
   }
 }
 
+/**
+ * 刷新用户状态(从后端获取最新信息)
+ * 各页面可调用此方法确保用户状态是最新的
+ * 如果用户被禁用(status=1),自动退出登录
+ * @returns {Promise<object|null>} 最新的用户信息或null
+ */
+export const refreshUserInfo = async () => {
+  if (!isLoggedIn()) {
+    return null
+  }
+  
+  try {
+    const result = await getUserInfoApi()
+    if (result.code === 200 && result.data) {
+      const userInfo = {
+        nickname: result.data.nickname || '',
+        avatar: result.data.avatar || '',
+        phone: result.data.phone || '',
+        status: result.data.status || 0
+      }
+      
+      // 检查是否被禁用(status=1)
+      if (userInfo.status === 1) {
+        console.log('[用户状态] 账号已被禁用,自动退出登录')
+        handleUserDisabled()
+        return null
+      }
+      
+      setUserInfo(userInfo)
+      console.log('[刷新用户状态] 成功, status:', userInfo.status)
+      return userInfo
+    }
+  } catch (error) {
+    console.warn('[刷新用户状态] 失败:', error.message)
+  }
+  return getUserInfo()
+}
+
+/**
+ * 处理用户被禁用的情况
+ */
+const handleUserDisabled = () => {
+  if (isShowingDisabledModal) {
+    return // 防止重复弹窗
+  }
+  
+  isShowingDisabledModal = true
+  stopStatusCheck() // 停止定时检查
+  logout() // 退出登录
+  
+  uni.showModal({
+    title: '账号已禁用',
+    content: '您的账号已被禁用,如有疑问请联系客服',
+    showCancel: false,
+    confirmText: '我知道了',
+    success: () => {
+      isShowingDisabledModal = false
+      // 跳转到个人中心
+      uni.switchTab({ url: '/pages/mine/mine' })
+    }
+  })
+}
+
+/**
+ * 启动全局用户状态定时检查
+ * 每10秒检查一次用户状态,如果被禁用则自动退出
+ */
+export const startStatusCheck = () => {
+  if (statusCheckTimer) {
+    return // 已经在运行
+  }
+  
+  if (!isLoggedIn()) {
+    return // 未登录不需要检查
+  }
+  
+  console.log('[状态检查] 启动定时检查')
+  
+  // 立即检查一次
+  checkUserStatus()
+  
+  // 定时检查
+  statusCheckTimer = setInterval(() => {
+    checkUserStatus()
+  }, STATUS_CHECK_INTERVAL)
+}
+
+/**
+ * 停止全局用户状态定时检查
+ */
+export const stopStatusCheck = () => {
+  if (statusCheckTimer) {
+    console.log('[状态检查] 停止定时检查')
+    clearInterval(statusCheckTimer)
+    statusCheckTimer = null
+  }
+}
+
+/**
+ * 检查用户状态(内部方法)
+ */
+const checkUserStatus = async () => {
+  if (!isLoggedIn()) {
+    stopStatusCheck()
+    return
+  }
+  
+  try {
+    const result = await getUserInfoApi()
+    if (result.code === 200 && result.data) {
+      // 检查是否被禁用(status=1)
+      if (result.data.status === 1) {
+        console.log('[状态检查] 检测到账号被禁用')
+        handleUserDisabled()
+      }
+    }
+  } catch (error) {
+    console.warn('[状态检查] 检查失败:', error.message)
+  }
+}
+
 /**
  * 检查登录状态,未登录则提示用户登录
  * @param {function} callback - 登录成功后的回调函数