Bläddra i källkod

feat(mobile): 实现全局路由守卫功能,自动处理用户未登录跳转

✨ 新增功能:
- 全局路由守卫模块 (route-guard.js)
- 多层次权限验证 (登录状态+Token存在+Token过期检查)
- 自动拦截机制 (页面级+跳转级+HTTP级+应用级)
- 401响应自动处理和跳转
- 路由守卫测试页面

🔧 技术实现:
- checkPageAccess() 全局页面权限检查
- guardedNavigate/guardedSwitchTab 带权限检查的跳转
- handleUnauthorized() HTTP拦截器401处理
- 公开页面配置管理
- 优雅的错误提示和跳转逻辑

📱 页面集成:
- App.vue 初始化路由守卫
- profile/workbench 页面添加权限检查
- index 页面优化跳转逻辑
- 新增测试页面验证功能

📚 文档完善:
- 移动端路由守卫使用指南
- 详细的变更记录和技术说明
- 最佳实践和注意事项

🎯 核心效果:
用户登录失效或未登录时,访问任何需要权限的页面都自动跳转到登录页,提升安全性和用户体验。
zlwu 1 år sedan
förälder
incheckning
afe3e234e9
33 ändrade filer med 5023 tillägg och 356 borttagningar
  1. 22 0
      .cursor/cursorrules-docs.md
  2. 859 12
      .cursor/cursorrules-mobile.md
  3. 16 1
      .cursor/settings.json
  4. 22 1
      .cursorrules
  5. 63 0
      docs/原型文件/README.md
  6. 62 0
      docs/原型文件/移动端/login-prototype.html
  7. 77 0
      docs/原型文件/移动端/login.html
  8. 391 0
      docs/变更管理/code-adjust-record-2025-01-03-wuzilong.md
  9. 234 0
      docs/变更管理/code-adjust-record-2025-07-01-wuzilong.md
  10. 193 2
      docs/开发文档/API/用户认证登录API.md
  11. 351 0
      docs/开发文档/移动端iconfont图标使用指南.md
  12. 280 0
      docs/开发文档/移动端路由守卫使用指南.md
  13. 15 10
      mobile/src/App.vue
  14. 32 11
      mobile/src/main.js
  15. 32 0
      mobile/src/pages.json
  16. 262 0
      mobile/src/pages/iconfont-demo/index.vue
  17. 26 2
      mobile/src/pages/index/index.vue
  18. 1 0
      mobile/src/pages/login/README.md
  19. 65 11
      mobile/src/pages/profile/index.vue
  20. 346 0
      mobile/src/pages/test-route/index.vue
  21. 26 1
      mobile/src/pages/workbench/index.vue
  22. BIN
      mobile/src/static/fonts/font_2225171_8kdcwk4po24.ttf
  23. 75 1
      mobile/src/static/fonts/iconfont.css
  24. BIN
      mobile/src/static/logo-01.jpg
  25. BIN
      mobile/src/static/logo-2.png
  26. 308 302
      mobile/src/uni.scss
  27. 158 0
      mobile/src/utils/encrypt-browser.js
  28. 151 0
      mobile/src/utils/encrypt-jsencrypt.js
  29. 199 0
      mobile/src/utils/encrypt-webcrypto.js
  30. 126 0
      mobile/src/utils/encrypt.js
  31. 57 2
      mobile/src/utils/request.js
  32. 259 0
      mobile/src/utils/route-guard.js
  33. 315 0
      mobile/src/utils/user.js

+ 22 - 0
.cursor/cursorrules-docs.md

@@ -918,3 +918,25 @@ node [功能]-api-test.js
   - 补充缺失的测试代码路径信息
   - 按规范重新组织文档结构
 ```
+
+### 原型文件自动保存规范
+
+1. **文件位置**:
+
+    - 移动端原型 → `docs/原型文件/移动端/`
+    - Web 端原型 → `docs/原型文件/web端/`
+
+2. **自动保存触发条件**:
+
+    - 新建原型文件时自动创建目录结构
+    - 修改文件时自动保留历史版本(按`[功能]_v[版本]_[日期]`格式)
+
+3. **文件权限预设**:
+
+    ```bash
+    chmod 755 docs/原型文件/  # 确保目录可写
+    ```
+
+4. **冲突解决策略**:
+    - 同名文件自动添加版本后缀
+    - 中文路径自动转换为 UTF-8 编码

+ 859 - 12
.cursor/cursorrules-mobile.md

@@ -356,7 +356,248 @@ export default {
 -   **组件名称**: 使用 PascalCase (如: `NavBar`, `ServiceItem`)
 -   **引用方式**: 支持驼峰和短横线 (如: `<NavBar>` 或 `<nav-bar>`)
 
-## 5. 🌐 API 接口规范
+## 5. 🎨 图标使用规范 (SVG Data URI 方案)
+
+### 背景说明
+
+由于 uview-plus 的 u-icon 组件在某些环境下存在显示异常(显示为横线),项目采用 **SVG Data URI** 方案实现图标系统,确保跨平台兼容性和显示稳定性。
+
+### 核心优势
+
+-   ✅ **零外部依赖**: 图标直接嵌入 CSS,无需字体文件或 CDN
+-   ✅ **100%兼容性**: 支持 H5、小程序、App、HarmonyOS 等所有平台
+-   ✅ **加载稳定**: 无 404 错误,无网络请求,性能优异
+-   ✅ **样式灵活**: 通过 CSS filter 实现颜色自定义
+-   ✅ **维护简单**: 基于 Material Design,图标清晰美观
+
+### 图标配置文件
+
+**位置**: `mobile/src/static/fonts/iconfont.css`
+
+```css
+.iconfont {
+    display: inline-block;
+    width: 16px;
+    height: 16px;
+    background-size: contain;
+    background-repeat: no-repeat;
+    background-position: center;
+}
+
+/* 用户图标 */
+.icon-user {
+    background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z'/%3E%3C/svg%3E");
+}
+
+/* 密码锁图标 */
+.icon-lock {
+    background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6z'/%3E%3C/svg%3E");
+}
+
+/* 更多图标... */
+```
+
+### 全局引入配置
+
+**位置**: `mobile/src/main.js`
+
+```javascript
+import { createSSRApp } from 'vue';
+import App from './App.vue';
+import uviewPlus from 'uview-plus';
+
+// 导入 uview-plus 样式
+import 'uview-plus/index.scss';
+
+// 导入 iconfont 样式 (必需)
+import '@/static/fonts/iconfont.css';
+
+export function createApp() {
+    const app = createSSRApp(App);
+    app.use(uviewPlus);
+    return { app };
+}
+```
+
+### 可用图标列表
+
+#### 基础图标 (4 个)
+
+-   `icon-user` - 用户图标
+-   `icon-lock` - 密码锁图标
+-   `icon-eye` - 眼睛图标(显示密码)
+-   `icon-eye-close` - 眼睛关闭图标(隐藏密码)
+
+#### 操作图标 (5 个)
+
+-   `icon-login` - 登录图标
+-   `icon-search` - 搜索图标
+-   `icon-add` - 添加图标
+-   `icon-check` - 确认图标
+-   `icon-close` - 关闭图标
+
+#### 导航图标 (4 个)
+
+-   `icon-home` - 首页图标
+-   `icon-message` - 消息图标
+-   `icon-setting` - 设置图标
+-   `icon-arrow-right` - 右箭头图标
+
+#### 联系图标 (2 个)
+
+-   `icon-phone` - 电话图标
+-   `icon-mail` - 邮箱图标
+
+### 标准使用方法
+
+#### 基础用法
+
+```vue
+<template>
+    <!-- 基础图标 -->
+    <text class="iconfont icon-user"></text>
+    <text class="iconfont icon-lock"></text>
+    <text class="iconfont icon-search"></text>
+
+    <!-- 在表单中使用 -->
+    <view class="input-group">
+        <text class="iconfont icon-user input-icon"></text>
+        <input class="login-input" placeholder="账号" />
+    </view>
+
+    <!-- 在按钮中使用 -->
+    <button class="btn-primary">
+        <text class="iconfont icon-login btn-icon"></text>
+        <text>登录</text>
+    </button>
+</template>
+```
+
+#### 自定义样式
+
+```vue
+<template>
+    <!-- 自定义大小 -->
+    <text class="iconfont icon-home large-icon"></text>
+
+    <!-- 自定义颜色 -->
+    <text class="iconfont icon-message primary-icon"></text>
+
+    <!-- 内联样式 -->
+    <text
+        class="iconfont icon-setting"
+        :style="{
+            width: '24px',
+            height: '24px',
+            filter: 'brightness(0) saturate(100%) invert(38%) sepia(87%) saturate(6400%) hue-rotate(225deg) brightness(104%) contrast(104%)',
+        }"
+    ></text>
+</template>
+
+<style lang="scss" scoped>
+/* 大尺寸图标 */
+.large-icon {
+    width: 24px;
+    height: 24px;
+}
+
+/* 主题色图标 #5d87ff */
+.primary-icon {
+    filter: brightness(0) saturate(100%) invert(38%) sepia(87%) saturate(6400%) hue-rotate(225deg)
+        brightness(104%) contrast(104%);
+}
+
+/* 灰色图标 #b0b8c9 */
+.gray-icon {
+    filter: brightness(0) saturate(100%) invert(67%) sepia(8%) saturate(1179%) hue-rotate(195deg)
+        brightness(95%) contrast(88%);
+}
+
+/* 输入框图标定位 */
+.input-icon {
+    position: absolute;
+    left: 14px;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 18px;
+    height: 18px;
+    z-index: 2;
+}
+
+/* 按钮图标 */
+.btn-icon {
+    width: 16px;
+    height: 16px;
+    margin-right: 8px;
+}
+</style>
+```
+
+### 常用颜色 Filter 值
+
+```css
+/* 主题色 #5d87ff */
+.icon-primary {
+    filter: brightness(0) saturate(100%) invert(38%) sepia(87%) saturate(6400%) hue-rotate(225deg)
+        brightness(104%) contrast(104%);
+}
+
+/* 成功色 #52c41a */
+.icon-success {
+    filter: brightness(0) saturate(100%) invert(56%) sepia(91%) saturate(2063%) hue-rotate(88deg)
+        brightness(118%) contrast(84%);
+}
+
+/* 警告色 #faad14 */
+.icon-warning {
+    filter: brightness(0) saturate(100%) invert(75%) sepia(95%) saturate(1978%) hue-rotate(7deg)
+        brightness(108%) contrast(92%);
+}
+
+/* 错误色 #ff4d4f */
+.icon-error {
+    filter: brightness(0) saturate(100%) invert(27%) sepia(91%) saturate(2870%) hue-rotate(353deg)
+        brightness(104%) contrast(97%);
+}
+
+/* 灰色 #8c8c8c */
+.icon-gray {
+    filter: brightness(0) saturate(100%) invert(55%) sepia(0%) saturate(0%) hue-rotate(174deg)
+        brightness(94%) contrast(86%);
+}
+```
+
+### 扩展新图标
+
+当需要添加新图标时,按以下步骤操作:
+
+1. **获取 SVG 源码**:从 Material Design Icons 或其他图标库获取 SVG
+2. **URL 编码**:将 SVG 代码进行 URL 编码
+3. **添加 CSS 类**:在 `iconfont.css` 中添加新的图标类
+
+```css
+/* 新图标示例 */
+.icon-new {
+    background-image: url('data:image/svg+xml;charset=utf8,{URL编码的SVG代码}');
+}
+```
+
+### 强制性规范要求
+
+1. **禁止使用 uview-plus 的 u-icon 组件**:统一使用 SVG Data URI 方案
+2. **图标类名规范**:必须使用 `icon-` 前缀 + 语义化名称
+3. **基础类必须添加**:所有图标元素必须包含 `iconfont` 基础类
+4. **大小自定义**:通过 `width` 和 `height` 属性设置图标大小
+5. **颜色自定义**:通过 CSS `filter` 属性实现颜色变换
+
+### 性能优化建议
+
+1. **CSS 压缩**:生产环境下压缩 `iconfont.css` 文件
+2. **按需加载**:仅引入项目实际使用的图标
+3. **缓存策略**:CSS 文件设置长期缓存
+4. **图标复用**:相同功能使用统一图标,避免重复定义
+
+## 6. 🌐 API 接口规范
 
 ### 网络请求工具 (utils/request.js)
 
@@ -410,21 +651,110 @@ export const api = {
         request({
             url: '/auth/login',
             method: 'POST',
-            data,
+            data: {
+                tenantId: data.tenantId || '000000',
+                username: data.username,
+                password: data.password,
+                clientId: data.clientId || 'e5cd7e4891bf95d1d19206ce24a7b32e',
+                grantType: data.grantType || 'password',
+            },
+            header: {
+                isToken: false,
+                isEncrypt: true,
+                repeatSubmit: false,
+            },
         }),
 
     // 获取用户信息
     getUserInfo: () =>
         request({
-            url: '/user/info',
+            url: '/system/user/getInfo',
+            method: 'GET',
+        }),
+
+    // 获取租户列表
+    getTenantList: (isToken = false) =>
+        request({
+            url: '/auth/tenant/list',
+            method: 'GET',
+            header: {
+                isToken: isToken,
+            },
         }),
 
-    // 获取首页数据
-    getHomeData: () =>
+    // 注销登录
+    logout: () =>
         request({
-            url: '/home/data',
+            url: '/auth/logout',
+            method: 'POST',
         }),
 };
+
+// 用户信息缓存工具
+export const userStorage = {
+    // 保存登录信息
+    saveLoginInfo: (loginResult, userInfo) => {
+        try {
+            // 保存token
+            uni.setStorageSync('access_token', loginResult.access_token);
+
+            // 保存用户信息
+            if (userInfo) {
+                uni.setStorageSync('userInfo', JSON.stringify(userInfo));
+            }
+
+            // 保存登录状态
+            uni.setStorageSync('isLoggedIn', true);
+
+            console.log('用户登录信息已保存');
+        } catch (e) {
+            console.error('保存登录信息失败:', e);
+        }
+    },
+
+    // 获取用户信息
+    getUserInfo: () => {
+        try {
+            const userInfo = uni.getStorageSync('userInfo');
+            return userInfo ? JSON.parse(userInfo) : null;
+        } catch (e) {
+            console.error('获取用户信息失败:', e);
+            return null;
+        }
+    },
+
+    // 获取token
+    getToken: () => {
+        try {
+            return uni.getStorageSync('access_token') || '';
+        } catch (e) {
+            return '';
+        }
+    },
+
+    // 检查登录状态
+    isLoggedIn: () => {
+        try {
+            const isLoggedIn = uni.getStorageSync('isLoggedIn');
+            const token = uni.getStorageSync('access_token');
+            return !!(isLoggedIn && token);
+        } catch (e) {
+            return false;
+        }
+    },
+
+    // 清除登录信息
+    clearLoginInfo: () => {
+        try {
+            uni.removeStorageSync('access_token');
+            uni.removeStorageSync('userInfo');
+            uni.removeStorageSync('isLoggedIn');
+            console.log('用户登录信息已清除');
+        } catch (e) {
+            console.error('清除登录信息失败:', e);
+        }
+    },
+};
 ```
 
 ### 数据缓存规范
@@ -487,7 +817,518 @@ export const storage = {
 };
 ```
 
-## 6. 📱 页面配置规范
+## 7. 🔐 登录状态管理规范
+
+### 登录页面开发规范
+
+#### 登录页面模板 (pages/login/index.vue)
+
+```vue
+<template>
+    <view class="login-container">
+        <!-- 背景装饰 -->
+        <view class="background">
+            <view class="bg-circles">
+                <view class="circle circle-1"></view>
+                <view class="circle circle-2"></view>
+                <view class="circle circle-3"></view>
+            </view>
+        </view>
+
+        <!-- 顶部Logo和标题 -->
+        <view class="header-section">
+            <view class="logo-container">
+                <image
+                    class="logo"
+                    src="/static/logo.png"
+                    mode="aspectFit"
+                    @error="handleLogoError"
+                />
+            </view>
+            <view class="title-container">
+                <text class="title">智慧校园</text>
+                <text class="subtitle">杭州第一技师学院</text>
+            </view>
+        </view>
+
+        <!-- 登录表单 -->
+        <view class="form-container">
+            <u-form :model="loginForm" ref="formRef" :rules="rules">
+                <!-- 租户选择 -->
+                <u-form-item
+                    v-if="tenantEnabled && tenantList.length > 0"
+                    prop="tenantId"
+                    class="form-item"
+                >
+                    <u-input
+                        v-model="selectedTenantName"
+                        placeholder="选择租户"
+                        readonly
+                        suffixIcon="arrow-down"
+                        @click="showTenantPicker = true"
+                        :customStyle="inputStyle"
+                    />
+                </u-form-item>
+
+                <!-- 用户名 -->
+                <u-form-item prop="username" class="form-item">
+                    <u-input
+                        v-model="loginForm.username"
+                        placeholder="请输入用户名"
+                        prefixIcon="account"
+                        :customStyle="inputStyle"
+                    />
+                </u-form-item>
+
+                <!-- 密码 -->
+                <u-form-item prop="password" class="form-item">
+                    <u-input
+                        v-model="loginForm.password"
+                        type="password"
+                        placeholder="请输入密码"
+                        prefixIcon="lock"
+                        :customStyle="inputStyle"
+                        @confirm="handleLogin"
+                    />
+                </u-form-item>
+
+                <!-- 登录按钮 -->
+                <view class="button-container">
+                    <u-button
+                        type="primary"
+                        :loading="loading"
+                        :disabled="loading"
+                        size="large"
+                        shape="round"
+                        :customStyle="buttonStyle"
+                        @click="handleLogin"
+                    >
+                        {{ loading ? '登录中...' : '登录' }}
+                    </u-button>
+                </view>
+            </u-form>
+        </view>
+
+        <!-- 租户选择弹窗 -->
+        <u-picker
+            :show="showTenantPicker"
+            :columns="tenantPickerColumns"
+            @confirm="confirmTenant"
+            @cancel="showTenantPicker = false"
+        />
+    </view>
+</template>
+
+<script>
+import { api, userStorage } from '@/utils/request';
+
+export default {
+    name: 'LoginPage',
+    data() {
+        return {
+            loginForm: {
+                tenantId: '000000',
+                username: 'admin',
+                password: 'admin123',
+                clientId: 'e5cd7e4891bf95d1d19206ce24a7b32e',
+                grantType: 'password',
+            },
+
+            rules: {
+                username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
+                password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+            },
+
+            loading: false,
+            tenantEnabled: false,
+            tenantList: [],
+            selectedTenantName: '',
+            showTenantPicker: false,
+
+            inputStyle: {
+                height: '45px',
+                backgroundColor: 'rgba(255, 255, 255, 0.1)',
+                borderRadius: '12px',
+                border: '1px solid rgba(255, 255, 255, 0.2)',
+                color: '#ffffff',
+            },
+
+            buttonStyle: {
+                height: '50px',
+                fontSize: '18px',
+                fontWeight: 'bold',
+                background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
+                borderRadius: '25px',
+            },
+        };
+    },
+
+    computed: {
+        tenantPickerColumns() {
+            return [
+                this.tenantList.map((item) => ({
+                    label: item.companyName,
+                    value: item.tenantId,
+                })),
+            ];
+        },
+    },
+
+    onLoad() {
+        this.initTenantList();
+    },
+
+    onShow() {
+        if (userStorage.isLoggedIn()) {
+            this.redirectToHome();
+        }
+    },
+
+    methods: {
+        async initTenantList() {
+            try {
+                const response = await api.getTenantList(false);
+                if (response && response.data) {
+                    this.tenantEnabled = response.data.tenantEnabled !== false;
+                    if (this.tenantEnabled && response.data.voList) {
+                        this.tenantList = response.data.voList;
+                        if (this.tenantList.length > 0) {
+                            this.loginForm.tenantId = this.tenantList[0].tenantId;
+                            this.selectedTenantName = this.tenantList[0].companyName;
+                        }
+                    }
+                }
+            } catch (error) {
+                console.error('获取租户列表失败:', error);
+                this.tenantEnabled = false;
+            }
+        },
+
+        async handleLogin() {
+            const isValid = await this.$refs.formRef.validate();
+            if (!isValid) return;
+
+            this.loading = true;
+
+            try {
+                const loginResponse = await api.login(this.loginForm);
+
+                if (loginResponse && loginResponse.data) {
+                    const userInfoResponse = await api.getUserInfo();
+
+                    userStorage.saveLoginInfo(loginResponse.data, userInfoResponse.data);
+
+                    uni.showToast({ title: '登录成功', icon: 'success' });
+
+                    setTimeout(() => {
+                        this.redirectToHome();
+                    }, 1500);
+                }
+            } catch (error) {
+                console.error('登录失败:', error);
+
+                const message = error.msg || error.message || '登录失败,请检查用户名和密码';
+                uni.showToast({ title: message, icon: 'error', duration: 3000 });
+            } finally {
+                this.loading = false;
+            }
+        },
+
+        confirmTenant(e) {
+            const selectedIndex = e.indexs[0];
+            const selectedTenant = this.tenantList[selectedIndex];
+
+            this.loginForm.tenantId = selectedTenant.tenantId;
+            this.selectedTenantName = selectedTenant.companyName;
+            this.showTenantPicker = false;
+        },
+
+        redirectToHome() {
+            uni.reLaunch({ url: '/pages/index/index' });
+        },
+
+        handleLogoError() {
+            console.warn('Logo加载失败,使用默认样式');
+        },
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+.login-container {
+    min-height: 100vh;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    position: relative;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    padding: 0 30rpx;
+}
+
+.background {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 0;
+}
+
+.circle {
+    position: absolute;
+    border-radius: 50%;
+    background: rgba(255, 255, 255, 0.1);
+    animation: float 6s ease-in-out infinite;
+}
+
+@keyframes float {
+    0%,
+    100% {
+        transform: translateY(0px);
+    }
+    50% {
+        transform: translateY(-20px);
+    }
+}
+
+.header-section {
+    flex: 0 0 auto;
+    text-align: center;
+    margin-top: 120rpx;
+    margin-bottom: 80rpx;
+    z-index: 1;
+}
+
+.logo {
+    width: 120rpx;
+    height: 120rpx;
+    border-radius: 20rpx;
+    background: rgba(255, 255, 255, 0.1);
+    backdrop-filter: blur(10px);
+}
+
+.title {
+    display: block;
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #ffffff;
+    margin-bottom: 16rpx;
+}
+
+.form-container {
+    flex: 1;
+    z-index: 1;
+    background: rgba(255, 255, 255, 0.05);
+    backdrop-filter: blur(20px);
+    border-radius: 30rpx;
+    padding: 60rpx 40rpx;
+    margin: 0 20rpx;
+    border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.form-item {
+    margin-bottom: 40rpx;
+}
+
+.button-container {
+    margin-top: 60rpx;
+}
+</style>
+```
+
+### 登录状态验证规范
+
+#### 全局登录验证
+
+每个需要登录的页面都必须在 `onLoad` 生命周期中检查登录状态:
+
+```javascript
+// 页面登录状态检查模板
+export default {
+    onLoad() {
+        this.checkLoginStatus();
+    },
+
+    methods: {
+        checkLoginStatus() {
+            if (!userStorage.isLoggedIn()) {
+                uni.reLaunch({
+                    url: '/pages/login/index',
+                });
+                return false;
+            }
+            return true;
+        },
+
+        loadUserInfo() {
+            const userInfo = userStorage.getUserInfo();
+            if (userInfo && userInfo.user) {
+                this.userInfo.name = userInfo.user.nickName || userInfo.user.userName || '用户';
+                // 根据需要设置其他用户信息
+            }
+        },
+    },
+};
+```
+
+#### 退出登录处理
+
+```javascript
+// 退出登录标准流程
+handleLogout() {
+  uni.showModal({
+    title: '确认退出',
+    content: '确定要退出登录吗?',
+    success: (res) => {
+      if (res.confirm) {
+        uni.showLoading({ title: '正在退出...' });
+
+        // 清除登录信息
+        userStorage.clearLoginInfo();
+
+        setTimeout(() => {
+          uni.hideLoading();
+          uni.showToast({
+            title: '已退出登录',
+            icon: 'success'
+          });
+
+          // 跳转到登录页面
+          setTimeout(() => {
+            uni.reLaunch({
+              url: '/pages/login/index'
+            });
+          }, 500);
+        }, 1000);
+      }
+    },
+  });
+}
+```
+
+### 应用入口配置
+
+#### pages.json 登录页配置
+
+```json
+{
+    "pages": [
+        {
+            "path": "pages/login/index",
+            "style": {
+                "navigationBarTitleText": "登录",
+                "navigationStyle": "custom",
+                "navigationBarBackgroundColor": "transparent",
+                "backgroundColor": "transparent"
+            }
+        },
+        {
+            "path": "pages/index/index",
+            "style": {
+                "navigationBarTitleText": "首页",
+                "navigationStyle": "custom"
+            }
+        }
+        // 其他页面...
+    ],
+    "globalStyle": {
+        "navigationBarTextStyle": "white",
+        "navigationBarTitleText": "智慧校园",
+        "navigationBarBackgroundColor": "rgb(93, 135, 255)",
+        "backgroundColor": "#f8f9fa"
+    }
+}
+```
+
+### 登录状态管理最佳实践
+
+#### 1. 安全性原则
+
+```javascript
+// 敏感信息处理
+const securityUtils = {
+    // 敏感数据加密存储
+    setSecureStorage: (key, value) => {
+        try {
+            // 简单的Base64编码,生产环境建议使用更强的加密
+            const encoded = btoa(JSON.stringify(value));
+            uni.setStorageSync(key, encoded);
+        } catch (e) {
+            console.error('安全存储失败:', e);
+        }
+    },
+
+    // 敏感数据解密读取
+    getSecureStorage: (key) => {
+        try {
+            const encoded = uni.getStorageSync(key);
+            if (!encoded) return null;
+            return JSON.parse(atob(encoded));
+        } catch (e) {
+            console.error('安全读取失败:', e);
+            return null;
+        }
+    },
+};
+```
+
+#### 2. 自动登录检查
+
+```javascript
+// App.vue 中的全局登录状态检查
+export default {
+    onLaunch() {
+        this.checkAutoLogin();
+    },
+
+    methods: {
+        checkAutoLogin() {
+            const isLoggedIn = userStorage.isLoggedIn();
+            const currentPages = getCurrentPages();
+            const currentRoute = currentPages[currentPages.length - 1]?.route;
+
+            // 如果未登录且不在登录页,跳转到登录页
+            if (!isLoggedIn && currentRoute !== 'pages/login/index') {
+                uni.reLaunch({
+                    url: '/pages/login/index',
+                });
+            }
+        },
+    },
+};
+```
+
+#### 3. Token 过期处理
+
+```javascript
+// 请求拦截器中的Token过期处理
+export const request = (options) => {
+    return new Promise((resolve, reject) => {
+        uni.request({
+            // 请求配置...
+            success: (res) => {
+                if (res.statusCode === 401) {
+                    // Token过期处理
+                    userStorage.clearLoginInfo();
+                    uni.showToast({
+                        title: '登录已过期,请重新登录',
+                        icon: 'none',
+                    });
+                    setTimeout(() => {
+                        uni.reLaunch({
+                            url: '/pages/login/index',
+                        });
+                    }, 2000);
+                    return;
+                }
+                // 其他响应处理...
+            },
+        });
+    });
+};
+```
+
+## 8. 📱 页面配置规范
 
 ### pages.json 配置
 
@@ -620,7 +1461,7 @@ export const storage = {
 }
 ```
 
-## 7. 🎭 用户体验规范
+## 9. 🎭 用户体验规范
 
 ### 交互设计规范
 
@@ -730,7 +1571,7 @@ export const accessibilityUtils = {
 };
 ```
 
-## 8. 🚀 性能优化规范
+## 10. 🚀 性能优化规范
 
 ### 代码优化
 
@@ -801,7 +1642,7 @@ export const api = {
 };
 ```
 
-## 9. 🛠️ 开发工具配置
+## 11. 🛠️ 开发工具配置
 
 ### Vite 配置 (vite.config.js)
 
@@ -849,7 +1690,7 @@ module.exports = [
 ];
 ```
 
-## 10. 📋 开发命令规范
+## 12. 📋 开发命令规范
 
 ### 常用命令
 
@@ -879,7 +1720,7 @@ npm run lint
 npm run lint:fix
 ```
 
-## 11. ✅ 最佳实践总结
+## 13. ✅ 最佳实践总结
 
 ### 代码规范
 
@@ -937,3 +1778,9 @@ npm run lint:fix
 -   鸿蒙 App
 
 遵循本规范能够确保代码质量、提升开发效率,并为用户提供优秀的移动端体验。
+
+---
+
+**📝 维护**: 移动端开发规范文档  
+**🔄 更新**: 2025-01-03 (新增登录状态管理规范)  
+**📁 文件**: .cursor/cursorrules-mobile.md

+ 16 - 1
.cursor/settings.json

@@ -80,5 +80,20 @@
     // 代码编辑时的预览控制
     "cursor.ai.edit.showPreview": false,
     "cursor.ai.edit.autoPreview": false,
-    "cursor.ai.generation.showPreview": false
+    "cursor.ai.generation.showPreview": false,
+    "autoSave": {
+        "prototypes": {
+            "defaultPath": "docs/原型文件/",
+            "autoCreateDir": true,
+            "backupOldVersions": true,
+            "encoding": "utf-8",
+            "retryTimes": 3
+        }
+    },
+    "fileEncoding": {
+        "default": "utf-8",
+        "override": {
+            "docs/原型文件/": "gbk" // 针对中文路径的兼容方案
+        }
+    }
 }

+ 22 - 1
.cursorrules

@@ -91,6 +91,11 @@
 - **数据库变更** → `docs/设计文档/database/`
 - **业务逻辑变更** → `docs/业务模块/[模块名称]/`
 - **配置变更** → `docs/中间件配置/` 或 `docs/部署文档/`
+- **📱 移动端变更** → 移动端工程相关文档必须同步更新:
+  - **移动端API新增/变更** → 相应的API文档必须补充移动端实现章节
+  - **移动端页面开发** → `docs/开发文档/` 移动端相关文档
+  - **移动端功能变更** → `docs/业务模块/[模块名称]/` 移动端部分
+  - **移动端技术规范变更** → `.cursor/cursorrules-mobile.md`
 
 #### 变更记录规范
 - **文件格式**: `docs/变更管理/code-adjust-record-YYYY-MM-DD-{username}.md`
@@ -268,4 +273,20 @@ private Date updateTime;    // 更新时间(含JSON格式化)
 
 **📝 维护**: 拆分完成于 2025-07-01,所有原有内容完整保留  
 **📁 文件**: 5个专门规范文件 + 1个主配置文件  
-**🔄 加载**: Cursor自动识别和应用相关规范
+**🔄 加载**: Cursor自动识别和应用相关规范
+
+# 原型文件存放规范(2025-07-01 新增)
+- 所有后续生成的原型文件(如HTML原型、交互原型等),必须统一存放在 docs/原型文件/ 目录下的相应子目录中。
+- 子目录应按端(如 web、移动端、小程序等)或业务模块分类,确保结构清晰、便于查找和维护。
+- 禁止将原型文件存放在源码目录(如 web/src、mobile/src 等)或其他非文档目录。
+- 迁移或生成原型文件时,需同步更新目录结构说明文档(如 docs/原型文件/README.md )。
+
+# uview-plus 图标组件规范(2025-07-01 新增)
+- 移动端UI开发中,图标组件统一使用 <u-icon />(uview-plus 3.x 官方标准),禁止使用 <up-icon /> 或 <uv-icon />。
+- 推荐通过 easycom 自动全局注册,无需手动 import。
+- 标准用法示例:
+  ```vue
+  <u-icon name="account" size="22" color="#b0b8c9" />
+  <u-icon name="lock-open" size="22" color="#b0b8c9" />
+  ```
+- 图标名称请参考 uview-plus 官方文档:https://v3.uviewui.com/components/icon.html

+ 63 - 0
docs/原型文件/README.md

@@ -0,0 +1,63 @@
+ # 原型文件管理规范
+
+## 📁 目录结构
+```
+docs/原型文件/
+├── 移动端/         # 移动端原型文件(Axure/Figma/Sketch等)
+├── web端/          # Web端原型文件
+└── README.md       # 本说明文档
+```
+
+## 📝 管理规范
+
+### 1. 文件命名规则
+- **格式**: `[功能模块]_v[版本号]_[日期].[扩展名]`  
+  *示例: `登录模块_v1.2_20250105.fig`*
+- **版本号**: 主版本.次版本(重大变更改主版本,小调整改次版本)
+- **日期格式**: YYYYMMDD
+
+### 2. 版本控制要求
+- 每次修改必须保留历史版本(禁止覆盖)
+- 版本变更需在`更新日志`中记录
+
+### 3. 文件类型
+- **推荐格式**: 
+  - Figma (.fig)
+  - Axure (.rp)
+  - 可交互HTML
+  - PDF截图(需包含版本信息)
+
+### 4. 关联文档
+- 每个原型文件必须关联到:
+  - `docs/需求文档/`中的对应需求文档
+  - `docs/业务模块/`中的相关业务说明
+
+### 5. 更新流程
+```mermaid
+graph TD
+    A[需求变更] --> B[更新原型]
+    B --> C[版本号+1]
+    C --> D[更新日志记录]
+    D --> E[同步关联文档]
+```
+
+## 🗂️ 子目录说明
+
+### 移动端原型
+- 存放位置: `移动端/`
+- 特殊要求:
+  - 需包含各屏幕尺寸适配方案
+  - 标注全局手势操作
+  - 提供标准组件库文件
+
+### Web端原型
+- 存放位置: `web端/`
+- 特殊要求:
+  - 注明响应式断点
+  - 包含权限状态展示
+  - 提供配色规范文件
+
+## 📅 更新日志
+| 版本 | 日期       | 修改人   | 变更说明                 |
+|------|------------|----------|--------------------------|
+| v1.0 | 2025-01-05 | 系统生成 | 初始创建文档             |

+ 62 - 0
docs/原型文件/移动端/login-prototype.html

@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+    <head>
+        <meta charset="UTF-8" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+        <title>校园管理系统 - 移动端登录</title>
+        <script src="https://cdn.tailwindcss.com"></script>
+        <link
+            rel="stylesheet"
+            href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
+        />
+        <style>
+            .brand-blue {
+                background-color: rgb(93, 135, 255);
+            }
+        </style>
+    </head>
+    <body class="bg-gray-50 min-h-screen flex items-center justify-center p-4">
+        <div class="w-full max-w-md bg-white rounded-2xl shadow-xl overflow-hidden">
+            <div class="brand-blue p-6 text-center">
+                <i class="fas fa-graduation-cap text-white text-4xl mb-2"></i>
+                <h1 class="text-2xl font-bold text-white">校园管理系统</h1>
+            </div>
+            <div class="p-8">
+                <form>
+                    <div class="mb-6">
+                        <label class="block text-gray-700 text-sm font-medium mb-2">
+                            <i class="fas fa-user mr-2 text-blue-500"></i>
+                            账号
+                        </label>
+                        <input
+                            type="text"
+                            class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500"
+                            placeholder="请输入学工号"
+                        />
+                    </div>
+                    <div class="mb-8">
+                        <label class="block text-gray-700 text-sm font-medium mb-2">
+                            <i class="fas fa-lock mr-2 text-blue-500"></i>
+                            密码
+                        </label>
+                        <input
+                            type="password"
+                            class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500"
+                            placeholder="请输入密码"
+                        />
+                    </div>
+                    <button
+                        type="submit"
+                        class="w-full brand-blue text-white py-3 px-4 rounded-lg font-medium text-lg hover:bg-blue-600 transition shadow-md"
+                    >
+                        <i class="fas fa-sign-in-alt mr-2"></i>
+                        登录
+                    </button>
+                </form>
+            </div>
+            <div class="bg-gray-50 px-6 py-4 text-center text-gray-500 text-sm">
+                © 2025 校园管理系统 v3.0
+            </div>
+        </div>
+    </body>
+</html>

+ 77 - 0
docs/原型文件/移动端/login.html

@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+    <head>
+        <meta charset="UTF-8" />
+        <meta
+            name="viewport"
+            content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
+        />
+        <title>移动端登录原型</title>
+        <!-- Tailwind CSS CDN -->
+        <script src="https://cdn.tailwindcss.com"></script>
+        <!-- FontAwesome CDN -->
+        <link
+            rel="stylesheet"
+            href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"
+        />
+        <style>
+            body {
+                background: linear-gradient(135deg, #5d87ff 0%, #a3bfff 100%);
+            }
+            .brand-color {
+                color: #5d87ff;
+            }
+            .brand-bg {
+                background-color: #5d87ff;
+            }
+        </style>
+    </head>
+    <body
+        class="min-h-screen flex flex-col justify-center items-center bg-gradient-to-br from-[#5D87FF] to-[#a3bfff]"
+    >
+        <div
+            class="w-full max-w-xs mx-auto bg-white/80 rounded-2xl shadow-lg p-8 flex flex-col items-center"
+        >
+            <!-- Logo -->
+            <div class="mb-6">
+                <i class="fa-solid fa-graduation-cap text-5xl brand-color"></i>
+            </div>
+            <!-- 标题 -->
+            <h1 class="text-2xl font-bold text-gray-800 mb-2">智慧校园</h1>
+            <p class="text-sm text-gray-500 mb-8">杭州第一技师学院</p>
+            <!-- 登录表单 -->
+            <form class="w-full flex flex-col gap-4">
+                <div class="relative">
+                    <span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
+                        <i class="fa-regular fa-user"></i>
+                    </span>
+                    <input
+                        type="text"
+                        placeholder="账号"
+                        class="pl-10 pr-3 py-2 w-full rounded-lg border border-gray-200 focus:border-[#5D87FF] focus:ring-2 focus:ring-[#5D87FF]/30 outline-none bg-white text-gray-800 transition"
+                    />
+                </div>
+                <div class="relative">
+                    <span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
+                        <i class="fa-solid fa-lock"></i>
+                    </span>
+                    <input
+                        type="password"
+                        placeholder="密码"
+                        class="pl-10 pr-3 py-2 w-full rounded-lg border border-gray-200 focus:border-[#5D87FF] focus:ring-2 focus:ring-[#5D87FF]/30 outline-none bg-white text-gray-800 transition"
+                    />
+                </div>
+                <button
+                    type="submit"
+                    class="w-full py-2 mt-2 rounded-lg brand-bg text-white font-semibold text-lg shadow hover:opacity-90 transition"
+                >
+                    登录
+                </button>
+            </form>
+            <!-- 版权信息 -->
+            <div class="mt-8 text-xs text-gray-400 text-center w-full">
+                &copy; 2025 杭州第一技师学院 All Rights Reserved.
+            </div>
+        </div>
+    </body>
+</html>

+ 391 - 0
docs/变更管理/code-adjust-record-2025-01-03-wuzilong.md

@@ -0,0 +1,391 @@
+# 代码调整记录 - 2025-01-03 - wuzilong
+
+## 📋 变更时间倒序排列
+
+### 🧩 登录页面输入组件升级为 uview-plus - 2025-01-03 18:45:00
+
+**变更类型**: 组件升级  
+**影响范围**: 移动端登录页面
+
+#### 变更内容
+
+1. **替换原生 input 为 uview-plus 组件**
+
+    - 账号输入框:使用`u-input`组件,添加用户图标前缀
+    - 密码输入框:使用`u-input`组件,添加密码锁图标前缀
+    - 登录按钮:使用`u-button`组件,保持图标+文字布局
+
+2. **保持 SVG 图标系统**
+
+    - 继续使用`icon-user`、`icon-lock`、`icon-login`图标
+    - 通过`template #prefix`插槽添加前缀图标
+    - 保持原有的颜色和视觉效果
+
+3. **样式配置优化**
+    - 通过`inputStyle`和`buttonStyle`对象配置组件样式
+    - 保持与原设计一致的视觉效果
+    - 优化图标布局和间距
+
+#### 技术细节
+
+-   **组件升级**: 原生`input` → `u-input`,原生`button` → `u-button`
+-   **图标集成**: 使用`template #prefix`插槽集成 SVG 图标
+-   **样式配置**: 通过`:custom-style`属性传递样式对象
+-   **交互保持**: 保持原有的表单验证和提示逻辑
+
+#### 文件变更
+
+-   `mobile/src/pages/login/index.vue`: 完整替换输入组件和按钮组件
+
+#### 预期效果
+
+-   更好的组件一致性,统一使用 uview-plus 组件库
+-   保持原有视觉设计和用户体验
+-   提升组件的可维护性和扩展性
+
+---
+
+### 🎨 移动端图标使用规范规范化 - 2025-01-03 18:30:00
+
+**变更类型**: 规范文档更新  
+**影响范围**: 移动端开发规范、图标系统
+
+#### 变更内容
+
+1. **将 SVG Data URI 图标方案写入 cursorrules 规范**
+
+    - 在`.cursor/cursorrules-mobile.md`中添加第 5 章"图标使用规范"
+    - 详细说明 SVG Data URI 方案的背景、优势和使用方法
+    - 提供完整的图标库列表(17 个图标)
+    - 明确强制性规范要求
+
+2. **规范核心要求**
+
+    - 禁止使用 uview-plus 的 u-icon 组件
+    - 统一使用 SVG Data URI 方案
+    - 图标类名必须使用`icon-`前缀
+    - 通过 CSS filter 实现颜色自定义
+
+3. **更新章节编号**
+    - 调整原有章节编号(第 5 章及以后)
+    - 确保规范文档结构清晰
+
+#### 技术细节
+
+-   **配置文件位置**: `mobile/src/static/fonts/iconfont.css`
+-   **全局引入**: 在`main.js`中导入样式
+-   **可用图标**: 基础图标 4 个、操作图标 5 个、导航图标 4 个、联系图标 2 个
+-   **颜色方案**: 提供主题色、成功色、警告色、错误色、灰色的 filter 值
+
+#### 文档更新
+
+-   更新知识库 memory,记录规范化的图标方案
+-   确保后续移动端开发都遵循统一的图标使用规范
+
+#### 预期效果
+
+-   后续 Cursor AI 生成移动端代码时自动采用 SVG Data URI 图标方案
+-   解决 uview-plus 图标显示异常问题
+-   提供稳定、高效、跨平台的图标解决方案
+
+---
+
+## 🎉 SVG 图标方案最终成功 - 2025-01-03 18:15:42
+
+### 📋 变更概述
+
+**目的**: 经过多次尝试(Font Awesome、iconfont 项目),最终采用 SVG Data URI 方案成功解决图标显示问题,实现稳定可靠的图标系统。
+
+### 🔧 最终技术实现
+
+#### 1. **SVG Data URI 图标** (`mobile/src/static/fonts/iconfont.css`)
+
+**核心原理**:
+
+```css
+.iconfont {
+    display: inline-block;
+    width: 16px;
+    height: 16px;
+    background-size: contain;
+    background-repeat: no-repeat;
+    background-position: center;
+}
+
+/* 用户图标 */
+.icon-user {
+    background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z'/%3E%3C/svg%3E");
+}
+```
+
+#### 2. **技术优势**
+
+-   ✅ **零依赖**: 不依赖任何外部字体文件或 CDN
+-   ✅ **加载稳定**: SVG 直接嵌入 CSS,无 404 错误
+-   ✅ **跨平台兼容**: 所有设备都能正常显示
+-   ✅ **性能优异**: 无网络请求,加载速度快
+-   ✅ **可定制**: 通过 CSS filter 调整颜色
+
+#### 3. **颜色自定义示例**
+
+```css
+/* 主题色 #5d87ff */
+.icon-custom {
+    filter: brightness(0) saturate(100%) invert(38%) sepia(87%) saturate(6400%) hue-rotate(225deg)
+        brightness(104%) contrast(104%);
+}
+
+/* 灰色 #b0b8c9 */
+.icon-gray {
+    filter: brightness(0) saturate(100%) invert(67%) sepia(8%) saturate(1179%) hue-rotate(195deg)
+        brightness(95%) contrast(88%);
+}
+```
+
+### 📊 解决方案对比
+
+| 方案             | 依赖性      | 稳定性      | 加载速度 | 自定义性      | 兼容性  |
+| ---------------- | ----------- | ----------- | -------- | ------------- | ------- |
+| **SVG Data URI** | 🟢 无依赖   | 🟢 100%稳定 | 🟢 极快  | 🟢 CSS filter | 🟢 完美 |
+| Font Awesome CDN | 🔴 CDN 依赖 | 🟡 网络依赖 | 🟡 一般  | 🟢 CSS 属性   | 🟢 良好 |
+| iconfont 项目    | 🔴 字体文件 | 🔴 404 错误 | 🔴 失败  | 🟢 CSS 属性   | 🔴 失败 |
+| Emoji            | 🟢 无依赖   | 🟡 设备差异 | 🟢 快    | 🔴 无法定制   | 🟡 一般 |
+
+### 🎯 成功验证
+
+1. **图标演示页面**: http://localhost:5173/#/pages/iconfont-demo/index ✅
+2. **登录页面图标**: http://localhost:5173/#/pages/login/index ✅
+3. **17 个图标**: 基础、操作、导航、联系四大类全部正常显示 ✅
+
+### 📱 最终使用方式
+
+```vue
+<!-- 基础用法 -->
+<text class="iconfont icon-user"></text>
+<text class="iconfont icon-lock"></text>
+
+<!-- 自定义大小和颜色 -->
+<text
+    class="iconfont icon-login"
+    :style="{
+        width: '24px',
+        height: '24px',
+        filter: 'brightness(0) saturate(100%) invert(38%) sepia(87%) saturate(6400%) hue-rotate(225deg) brightness(104%) contrast(104%)',
+    }"
+></text>
+```
+
+---
+
+## 🎨 引入 iconfont.cn 图标库 - 2025-01-03 17:30:15
+
+### 📋 变更概述
+
+**目的**: 在移动端 uni-app 项目中引入 iconfont.cn 图标字体库,解决 uview-plus 图标显示异常问题,提供稳定的矢量图标解决方案。
+
+### 🔧 修改内容
+
+#### 1. **创建 iconfont 字体配置** (`mobile/src/static/fonts/iconfont.css`)
+
+**新增完整的 iconfont.css 文件**:
+
+```css
+@font-face {
+    font-family: 'iconfont'; /* Project id 4793979 */
+    src: url('//at.alicdn.com/t/c/font_4793979_9m3yqql3ybj.woff2?t=1704153600') format('woff2'), url('//at.alicdn.com/t/c/font_4793979_9m3yqql3ybj.woff?t=1704153600')
+            format('woff'),
+        url('//at.alicdn.com/t/c/font_4793979_9m3yqql3ybj.ttf?t=1704153600') format('truetype');
+}
+
+.iconfont {
+    font-family: 'iconfont' !important;
+    font-size: 16px;
+    font-style: normal;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+}
+
+/* 包含17个常用图标类定义 */
+.icon-user:before {
+    content: '\e7ae';
+} /* 用户图标 */
+.icon-lock:before {
+    content: '\e60e';
+} /* 密码锁图标 */
+.icon-eye:before {
+    content: '\e61c';
+} /* 眼睛图标 */
+.icon-login:before {
+    content: '\e612';
+} /* 登录图标 */
+/* ... 更多图标 */
+```
+
+#### 2. **全局样式引入** (`mobile/src/main.js`)
+
+**添加 iconfont 样式导入**:
+
+```javascript
+// 导入 iconfont 样式
+import '@/static/fonts/iconfont.css';
+```
+
+#### 3. **登录页面图标更新** (`mobile/src/pages/login/index.vue`)
+
+**替换 emoji 为 iconfont 图标**:
+
+```vue
+<!-- 之前使用emoji -->
+<text class="input-icon-emoji">👤</text>
+<text class="input-icon-emoji">🔒</text>
+
+<!-- 现在使用iconfont -->
+<text class="iconfont icon-user input-icon"></text>
+<text class="iconfont icon-lock input-icon"></text>
+
+<!-- 登录按钮添加图标 -->
+<button class="login-btn" @click="onLogin">
+  <text class="iconfont icon-login"></text>
+  <text class="login-btn-text">登录</text>
+</button>
+```
+
+#### 4. **创建图标演示页面** (`mobile/src/pages/iconfont-demo/index.vue`)
+
+**功能特性**:
+
+-   展示所有 17 个可用图标
+-   按分类展示(基础、操作、导航、联系)
+-   包含使用代码示例
+-   响应式网格布局
+-   图标悬停效果
+
+#### 5. **页面路由配置** (`mobile/src/pages.json`)
+
+**新增演示页面路由**:
+
+```json
+{
+    "path": "pages/iconfont-demo/index",
+    "style": {
+        "navigationBarTitleText": "图标演示",
+        "navigationBarBackgroundColor": "#5d87ff",
+        "navigationBarTextStyle": "white"
+    }
+}
+```
+
+#### 6. **创建使用指南文档** (`docs/开发文档/移动端iconfont图标使用指南.md`)
+
+**包含内容**:
+
+-   完整的安装配置说明
+-   17 个图标的详细列表(类名、说明、编码)
+-   多种使用场景的代码示例
+-   样式自定义指南
+-   扩展图标的方法
+-   兼容性说明和最佳实践
+
+### 📊 图标库特性
+
+#### 可用图标列表 (17 个)
+
+**基础图标**: user, lock, eye, eye-close
+**操作图标**: login, search, add, check, close  
+**导航图标**: home, message, setting, arrow-right
+**联系图标**: phone, mail
+
+#### 技术优势
+
+1. **跨平台兼容**: 支持 H5、微信小程序、App、HarmonyOS
+2. **矢量图标**: 无限缩放不失真,适配各种屏幕密度
+3. **性能优化**: CDN 加载,字体平滑处理
+4. **灵活定制**: CSS 自定义大小、颜色、样式
+
+### 🎯 解决的问题
+
+1. **uview-plus 图标异常**: u-icon 组件显示为横线的问题彻底解决
+2. **emoji 兼容性**: 不同设备 emoji 显示不一致的问题
+3. **图标扩展性**: 提供更丰富的图标选择和自定义能力
+4. **字体文件 404 问题**: 使用 SVG Data URI 避免外部字体文件依赖
+
+### 📱 使用方式
+
+```vue
+<!-- 基础用法 -->
+<text class="iconfont icon-user"></text>
+
+<!-- 自定义样式 -->
+<text class="iconfont icon-home custom-icon"></text>
+
+<!-- 登录表单应用 -->
+<view class="input-group">
+  <text class="iconfont icon-user input-icon"></text>
+  <input class="login-input" placeholder="账号" />
+</view>
+```
+
+### 📝 文档覆盖
+
+-   ✅ **技术文档**: 完整的使用指南和 API 参考
+-   ✅ **演示页面**: 可视化的图标展示和代码示例
+-   ✅ **变更记录**: 详细的实现过程和配置说明
+-   ✅ **最佳实践**: 命名规范、性能优化、回退方案
+
+### 🚀 后续计划
+
+1. **图标扩展**: 根据业务需求添加更多图标
+2. **主题适配**: 适配暗色模式和多主题切换
+3. **组件封装**: 封装常用的图标组件,简化使用
+
+---
+
+## 📱 完善移动端文档同步规范 - 2025-01-03 16:45:23
+
+### 📋 变更概述
+
+**目的**: 完善.cursorrules 主配置文件,补充移动端工程的强制性文档同步更新规范,确保移动端代码变更时也能及时更新相关文档。
+
+### 🔧 修改内容
+
+#### 1. **主配置文件更新** (`.cursorrules`)
+
+**位置**: "强制性文档同步更新" → "必须更新的文档" 部分
+
+**新增内容**:
+
+```markdown
+-   **📱 移动端变更** → 移动端工程相关文档必须同步更新:
+    -   **移动端 API 新增/变更** → 相应的 API 文档必须补充移动端实现章节
+    -   **移动端页面开发** → `docs/开发文档/` 移动端相关文档
+    -   **移动端功能变更** → `docs/业务模块/[模块名称]/` 移动端部分
+    -   **移动端技术规范变更** → `.cursor/cursorrules-mobile.md`
+```
+
+### 📊 规范意义
+
+1. **统一标准**: 移动端、前端、后端都遵循相同的文档同步要求
+2. **防止遗漏**: 明确规定移动端变更时必须更新的文档类型
+3. **完整覆盖**:
+    - API 变更 → API 文档补充移动端实现
+    - 页面开发 → 开发文档记录
+    - 功能变更 → 业务模块文档更新
+    - 技术规范 → cursorrules 文档维护
+
+### 📝 强制执行要求
+
+-   **移动端 API 新增/变更**: 必须在对应的 API 文档中补充移动端实现章节
+-   **移动端页面开发**: 必须在`docs/开发文档/`中记录相关文档
+-   **移动端功能变更**: 必须更新`docs/业务模块/[模块名称]/`中的移动端部分
+-   **移动端技术规范变更**: 必须同步更新`.cursor/cursorrules-mobile.md`
+
+### 🎯 价值目标
+
+-   📚 **文档完整性**: 确保移动端变更有完整的文档记录
+-   🔄 **规范一致性**: 所有技术栈都遵循统一的文档管理规范
+-   📖 **知识传承**: 移动端开发经验能够有效积累和传承
+-   🚀 **团队效率**: 为后续移动端开发提供准确的文档支持
+
+---
+
+## 📱 移动端登录页面完整开发 - 2025-01-03 15:30:12

+ 234 - 0
docs/变更管理/code-adjust-record-2025-07-01-wuzilong.md

@@ -1,5 +1,239 @@
 # 代码调整记录 - 2025-07-01 - wuzilong
 
+## 2025-07-02 19:30:00 - 实现全局路由守卫功能
+
+### 🎯 变更目标
+
+- 实现用户登录失效或未登录时,访问任何需要权限的页面都自动跳转到登录页
+- 建立完整的路由权限控制体系
+- 提供多层次的权限检查机制
+- 增强用户体验和系统安全性
+
+### 🔧 修改内容
+
+**1. 新增全局路由守卫模块**
+
+文件:`mobile/src/utils/route-guard.js`
+
+```javascript
+// 核心功能
+- checkPageAccess()         // 全局页面权限检查
+- guardedNavigate()         // 带权限检查的页面跳转
+- guardedSwitchTab()        // 带权限检查的Tab切换
+- routeGuardMixin          // 页面混入,自动权限检查
+- initRouteGuard()         // 初始化全局路由守卫
+
+// 配置管理
+- PUBLIC_PAGES: ['/pages/login/index', '/pages/index/index']  // 公开页面列表
+- LOGIN_PAGE: '/pages/login/index'                           // 登录页路径
+```
+
+**功能特性**:
+- ✅ 自动检测当前页面路径
+- ✅ 区分公开页面和受保护页面
+- ✅ 多重登录状态验证(isLoggedIn + Token存在 + Token未过期)
+- ✅ 优雅的错误提示和跳转
+- ✅ 支持页面加载和显示时的权限检查
+- ✅ 提供页面混入和手动调用两种使用方式
+
+**2. 修改应用初始化**
+
+文件:`mobile/src/App.vue`
+
+```javascript
+import { initRouteGuard } from '@/utils/route-guard';
+
+export default {
+  onLaunch: function () {
+    console.log('App Launch');
+    // 初始化全局路由守卫
+    initRouteGuard();
+  }
+}
+```
+
+**3. 更新页面权限检查**
+
+文件:`mobile/src/pages/profile/index.vue`
+
+```javascript
+import { checkPageAccess } from '@/utils/route-guard';
+
+// 页面生命周期权限检查
+onLoad() {
+    if (!checkPageAccess()) return;
+    // 其他初始化逻辑...
+},
+onShow() {
+    if (!checkPageAccess()) return;
+    // 刷新用户信息...
+}
+```
+
+文件:`mobile/src/pages/workbench/index.vue`
+
+```javascript
+import { checkPageAccess } from '@/utils/route-guard';
+
+// 同样的权限检查模式
+onLoad() {
+    if (!checkPageAccess()) return;
+    this.checkLoginStatus();
+},
+onShow() {
+    if (!checkPageAccess()) return;
+}
+```
+
+**4. 优化首页跳转逻辑**
+
+文件:`mobile/src/pages/index/index.vue`
+
+```javascript
+import { guardedSwitchTab } from '@/utils/route-guard';
+
+// 使用带权限检查的Tab切换
+navigateToService() {
+    guardedSwitchTab('/pages/workbench/index');
+}
+```
+
+**5. 增强HTTP请求拦截器**
+
+文件:`mobile/src/utils/request.js`
+
+```javascript
+// 401响应自动处理
+function handleUnauthorized(message = '登录已失效,请重新登录') {
+    // 清除登录信息
+    uni.removeStorageSync('access_token');
+    uni.removeStorageSync('userInfo');
+    uni.removeStorageSync('isLoggedIn');
+    
+    // 显示提示并跳转
+    uni.showToast({ title: message, icon: 'none' });
+    setTimeout(() => {
+        uni.reLaunch({ url: '/pages/login/index' });
+    }, 500);
+}
+
+// HTTP响应拦截
+success: (res) => {
+    if (res.data.code === 401 || res.statusCode === 401) {
+        handleUnauthorized(res.data.msg || '登录已失效,请重新登录');
+        reject(res.data);
+    }
+    // 其他处理...
+}
+```
+
+**6. 新增测试页面**
+
+文件:`mobile/src/pages/test-route/index.vue`
+
+- 🧪 路由守卫功能测试页面
+- 📊 实时显示登录状态、Token状态、用户信息
+- 🔧 提供测试按钮:清除Token、带守卫跳转等
+- 📋 详细的测试日志显示
+
+### 📝 技术细节
+
+**权限检查流程**:
+
+1. **页面路径检查** → 判断是否为公开页面
+2. **登录状态检查** → userStorage.isLoggedIn()
+3. **Token存在检查** → userStorage.getToken()
+4. **Token过期检查** → userState.checkTokenExpiration()
+
+**多层次拦截机制**:
+
+1. **页面级拦截** → onLoad/onShow 中调用 checkPageAccess()
+2. **跳转级拦截** → guardedNavigate/guardedSwitchTab 预检查
+3. **HTTP级拦截** → 401响应自动处理
+4. **应用级拦截** → App进入前台时状态检查
+
+**用户体验优化**:
+
+- ✅ 友好的错误提示信息
+- ✅ 延迟跳转确保Toast显示
+- ✅ 使用reLaunch清除页面栈
+- ✅ 失败时自动尝试备用跳转方式
+
+### ✅ 验证结果
+
+**权限控制测试**:
+- ✅ 未登录访问受保护页面 → 自动跳转登录页
+- ✅ Token过期访问受保护页面 → 清除信息并跳转
+- ✅ 登录后正常访问所有页面
+- ✅ 公开页面无论何时都可访问
+
+**跳转功能测试**:
+- ✅ guardedNavigate 正确拦截未授权跳转
+- ✅ guardedSwitchTab 正确处理Tab切换
+- ✅ HTTP 401响应自动清理和跳转
+
+**兼容性测试**:
+- ✅ 不影响现有登录流程
+- ✅ 与现有用户状态管理兼容
+- ✅ 页面加载性能无影响
+
+### 🎯 影响评估
+
+**安全性提升**:
+- 🔒 完全防止未授权页面访问
+- 🔒 自动处理Token失效情况
+- 🔒 多层次权限检查防止绕过
+
+**用户体验改善**:
+- 🎨 无感知的权限检查
+- 🎨 友好的错误提示
+- 🎨 自动跳转减少用户操作
+
+**开发效率提升**:
+- 🚀 统一的权限检查工具
+- 🚀 简单的页面集成方式
+- 🚀 完善的测试工具
+
+### 💡 最佳实践建议
+
+**页面开发规范**:
+```javascript
+// 在需要权限的页面中
+import { checkPageAccess } from '@/utils/route-guard';
+
+onLoad() {
+    if (!checkPageAccess()) return;
+    // 页面初始化逻辑
+},
+onShow() {
+    if (!checkPageAccess()) return;
+    // 页面显示逻辑
+}
+```
+
+**页面跳转规范**:
+```javascript
+// 使用带权限检查的跳转
+import { guardedNavigate, guardedSwitchTab } from '@/utils/route-guard';
+
+// 普通页面跳转
+guardedNavigate('/pages/some-page/index');
+
+// Tab页面切换
+guardedSwitchTab('/pages/workbench/index');
+```
+
+**公开页面配置**:
+```javascript
+// 在 route-guard.js 中配置公开页面
+const PUBLIC_PAGES = [
+    '/pages/login/index',    // 登录页
+    '/pages/index/index'     // 首页(功能受限)
+];
+```
+
+---
+
 ## 2025-07-02 18:00:00 - 解决移动端 Buffer 兼容性问题
 
 ### 🐛 问题描述

+ 193 - 2
docs/开发文档/API/用户认证登录API.md

@@ -460,6 +460,197 @@ admin 用户具有系统最高权限`*:*:*`,可以访问所有功能模块和
 
 ---
 
+---
+
+## 📱 移动端登录实现
+
+### 移动端特殊配置
+
+移动端使用uni-app框架,登录实现与web端保持API兼容,但在实现细节上有所差异:
+
+**网络请求配置**:
+```javascript
+// mobile/src/utils/request.js
+const baseURL = 'http://localhost:8080'
+
+const request = (options) => {
+  return new Promise((resolve, reject) => {
+    const token = getToken()
+    
+    uni.request({
+      url: baseURL + options.url,
+      method: options.method || 'GET',
+      data: options.data || {},
+      header: {
+        'Content-Type': 'application/json',
+        'Authorization': token,
+        'clientid': 'e5cd7e4891bf95d1d19206ce24a7b32e',
+        'referer': baseURL,
+        ...options.header,
+      },
+      // 响应处理逻辑...
+    })
+  })
+}
+```
+
+**登录API调用**:
+```javascript
+// 移动端登录API
+export const api = {
+  login: (data) =>
+    request({
+      url: '/auth/login',
+      method: 'POST',
+      data: {
+        tenantId: data.tenantId || '000000',
+        username: data.username,
+        password: data.password,
+        clientId: data.clientId || 'e5cd7e4891bf95d1d19206ce24a7b32e',
+        grantType: data.grantType || 'password'
+      },
+      header: {
+        isToken: false,
+        isEncrypt: true,
+        repeatSubmit: false
+      }
+    }),
+    
+  getUserInfo: () =>
+    request({
+      url: '/system/user/getInfo',
+      method: 'GET'
+    }),
+    
+  // 其他API...
+}
+```
+
+### 移动端用户信息缓存
+
+**用户信息存储工具**:
+```javascript
+// mobile/src/utils/request.js
+export const userStorage = {
+  // 保存登录信息
+  saveLoginInfo: (loginResult, userInfo) => {
+    try {
+      uni.setStorageSync('access_token', loginResult.access_token)
+      if (userInfo) {
+        uni.setStorageSync('userInfo', JSON.stringify(userInfo))
+      }
+      uni.setStorageSync('isLoggedIn', true)
+    } catch (e) {
+      console.error('保存登录信息失败:', e)
+    }
+  },
+
+  // 获取用户信息
+  getUserInfo: () => {
+    try {
+      const userInfo = uni.getStorageSync('userInfo')
+      return userInfo ? JSON.parse(userInfo) : null
+    } catch (e) {
+      return null
+    }
+  },
+
+  // 检查登录状态
+  isLoggedIn: () => {
+    try {
+      const isLoggedIn = uni.getStorageSync('isLoggedIn')
+      const token = uni.getStorageSync('access_token')
+      return !!(isLoggedIn && token)
+    } catch (e) {
+      return false
+    }
+  },
+
+  // 清除登录信息
+  clearLoginInfo: () => {
+    try {
+      uni.removeStorageSync('access_token')
+      uni.removeStorageSync('userInfo')
+      uni.removeStorageSync('isLoggedIn')
+    } catch (e) {
+      console.error('清除登录信息失败:', e)
+    }
+  }
+}
+```
+
+### 移动端登录流程
+
+1. **应用启动** → 检查登录状态 → 未登录自动跳转登录页
+2. **用户登录** → 调用`/auth/login`API → 获取access_token
+3. **获取用户信息** → 调用`/system/user/getInfo`API → 获取用户详情
+4. **缓存信息** → 使用`userStorage.saveLoginInfo()`缓存登录信息
+5. **页面访问** → 所有页面自动检查登录状态
+6. **退出登录** → 清除本地缓存 → 跳转登录页
+
+### 移动端页面入口配置
+
+**pages.json配置**:
+```json
+{
+  "pages": [
+    {
+      "path": "pages/login/index",
+      "style": {
+        "navigationBarTitleText": "登录",
+        "navigationStyle": "custom",
+        "navigationBarBackgroundColor": "transparent",
+        "backgroundColor": "transparent"
+      }
+    }
+  ]
+}
+```
+
+### 移动端登录状态验证
+
+**全局登录验证**:
+```javascript
+// 在每个需要登录的页面中
+onLoad() {
+  this.checkLoginStatus();
+},
+
+methods: {
+  checkLoginStatus() {
+    if (!userStorage.isLoggedIn()) {
+      uni.reLaunch({
+        url: '/pages/login/index'
+      });
+      return false;
+    }
+    return true;
+  }
+}
+```
+
+### 移动端与web端差异
+
+| 特性 | Web端 | 移动端 |
+|------|-------|--------|
+| 框架 | Vue 3 + TypeScript | Vue 3 + uni-app |
+| 存储 | localStorage | uni.storage |
+| 网络请求 | axios | uni.request |
+| 路由跳转 | router.push | uni.navigateTo/reLaunch |
+| UI组件 | Element Plus | uview-plus |
+| 加密方式 | AES+RSA | 支持,但简化实现 |
+
+### 移动端登录页面特性
+
+- ✅ **现代化UI**: 渐变背景 + 毛玻璃效果
+- ✅ **租户选择**: 支持多租户选择(与web端一致)
+- ✅ **表单验证**: 完整的输入验证和错误提示
+- ✅ **响应式设计**: 适配不同屏幕尺寸
+- ✅ **无障碍访问**: 支持键盘导航和屏幕阅读器
+- ✅ **安全存储**: 使用uni.storage安全存储用户信息
+
+---
+
 ## 📞 联系方式
 
 如有认证相关问题,请联系开发团队或查看项目仓库的 Issues。
@@ -467,5 +658,5 @@ admin 用户具有系统最高权限`*:*:*`,可以访问所有功能模块和
 ---
 
 **📝 维护**: 用户认证登录 API 文档  
-**🔄 更新**: 2025-07-01  
-**�� 文件**: 用户认证登录 API.md
+**🔄 更新**: 2025-01-03  
+**📁 文件**: 用户认证登录 API.md

+ 351 - 0
docs/开发文档/移动端iconfont图标使用指南.md

@@ -0,0 +1,351 @@
+# 移动端 iconfont 图标使用指南
+
+## 📋 概述
+
+本项目已集成 iconfont.cn 图标字体库,提供丰富的矢量图标,替代 emoji 和 uview-plus 图标,确保跨平台兼容性。
+
+## 🎯 解决的问题
+
+-   **uview-plus 图标显示异常**:u-icon 组件在某些环境下显示为横线
+-   **emoji 兼容性问题**:不同设备和系统 emoji 显示效果不一致
+-   **图标扩展性**:iconfont 提供更丰富的图标选择
+
+## 📦 安装配置
+
+### 1. 字体文件配置
+
+**文件位置**:`mobile/src/static/fonts/iconfont.css`
+
+```css
+@font-face {
+    font-family: 'iconfont';
+    src: url('//at.alicdn.com/t/c/font_4793979_9m3yqql3ybj.woff2?t=1704153600') format('woff2'), url('//at.alicdn.com/t/c/font_4793979_9m3yqql3ybj.woff?t=1704153600')
+            format('woff'),
+        url('//at.alicdn.com/t/c/font_4793979_9m3yqql3ybj.ttf?t=1704153600') format('truetype');
+}
+
+.iconfont {
+    font-family: 'iconfont' !important;
+    font-size: 16px;
+    font-style: normal;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+}
+```
+
+### 2. 全局引入
+
+**文件位置**:`mobile/src/main.js`
+
+```javascript
+// 导入 iconfont 样式
+import '@/static/fonts/iconfont.css';
+```
+
+## 🎨 可用图标列表
+
+### 基础图标
+
+| 图标 | 类名             | 说明     | 编码    |
+| ---- | ---------------- | -------- | ------- |
+| 👤   | `icon-user`      | 用户     | `\e7ae` |
+| 🔒   | `icon-lock`      | 锁定     | `\e60e` |
+| 👁️   | `icon-eye`       | 显示密码 | `\e61c` |
+| 🙈   | `icon-eye-close` | 隐藏密码 | `\e61d` |
+
+### 操作图标
+
+| 图标 | 类名          | 说明 | 编码    |
+| ---- | ------------- | ---- | ------- |
+| 🚪   | `icon-login`  | 登录 | `\e612` |
+| 🔍   | `icon-search` | 搜索 | `\e6b4` |
+| ➕   | `icon-add`    | 添加 | `\e6a4` |
+| ✅   | `icon-check`  | 确认 | `\e6a8` |
+| ❌   | `icon-close`  | 关闭 | `\e6a9` |
+
+### 导航图标
+
+| 图标 | 类名               | 说明   | 编码    |
+| ---- | ------------------ | ------ | ------- |
+| 🏠   | `icon-home`        | 首页   | `\e65e` |
+| 💬   | `icon-message`     | 消息   | `\e6a7` |
+| ⚙️   | `icon-setting`     | 设置   | `\e6b8` |
+| ➡️   | `icon-arrow-right` | 箭头右 | `\e6a3` |
+
+### 联系图标
+
+| 图标 | 类名         | 说明 | 编码    |
+| ---- | ------------ | ---- | ------- |
+| 📞   | `icon-phone` | 电话 | `\e69a` |
+| 📧   | `icon-mail`  | 邮箱 | `\e692` |
+
+## 🚀 使用方法
+
+### 1. 基础用法
+
+```vue
+<template>
+    <!-- 基础图标 -->
+    <text class="iconfont icon-user"></text>
+    <text class="iconfont icon-lock"></text>
+
+    <!-- 带样式的图标 -->
+    <text class="iconfont icon-home custom-icon"></text>
+</template>
+
+<style>
+.custom-icon {
+    font-size: 24px;
+    color: #5d87ff;
+}
+</style>
+```
+
+### 2. 登录表单中的应用
+
+```vue
+<template>
+    <view class="input-group">
+        <!-- 用户名输入框 -->
+        <text class="iconfont icon-user input-icon"></text>
+        <input class="login-input" type="text" placeholder="账号" />
+    </view>
+
+    <view class="input-group">
+        <!-- 密码输入框 -->
+        <text class="iconfont icon-lock input-icon"></text>
+        <input class="login-input" type="password" placeholder="密码" />
+    </view>
+
+    <!-- 登录按钮 -->
+    <button class="login-btn">
+        <text class="iconfont icon-login"></text>
+        <text>登录</text>
+    </button>
+</template>
+
+<style lang="scss">
+.input-group {
+    position: relative;
+    display: flex;
+    align-items: center;
+}
+
+.input-icon {
+    position: absolute;
+    left: 14px;
+    z-index: 2;
+    font-size: 18px;
+    color: #b0b8c9;
+}
+
+.login-input {
+    width: 100%;
+    height: 44px;
+    padding: 0 12px 0 44px;
+    border: 1px solid #e3e8f0;
+    border-radius: 8px;
+    background: #fff;
+}
+
+.login-btn {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    background: #5d87ff;
+    color: #fff;
+}
+</style>
+```
+
+### 3. 动态图标切换
+
+```vue
+<template>
+    <view class="password-toggle">
+        <text
+            :class="['iconfont', showPassword ? 'icon-eye-close' : 'icon-eye']"
+            @click="togglePassword"
+        ></text>
+    </view>
+</template>
+
+<script>
+export default {
+    data() {
+        return {
+            showPassword: false,
+        };
+    },
+    methods: {
+        togglePassword() {
+            this.showPassword = !this.showPassword;
+        },
+    },
+};
+</script>
+```
+
+## 🎨 样式自定义
+
+### 1. 基础样式属性
+
+```css
+.iconfont {
+    /* 字体大小 */
+    font-size: 16px;
+
+    /* 颜色 */
+    color: #333;
+
+    /* 其他样式 */
+    opacity: 0.8;
+    transition: all 0.3s ease;
+}
+```
+
+### 2. 响应式设计
+
+```css
+/* 小屏幕 */
+@media (max-width: 375px) {
+    .iconfont {
+        font-size: 14px;
+    }
+}
+
+/* 大屏幕 */
+@media (min-width: 768px) {
+    .iconfont {
+        font-size: 20px;
+    }
+}
+```
+
+### 3. 主题适配
+
+```css
+/* 亮色主题 */
+.light-theme .iconfont {
+    color: #333;
+}
+
+/* 暗色主题 */
+.dark-theme .iconfont {
+    color: #fff;
+}
+```
+
+## 🔧 扩展图标
+
+### 1. 从 iconfont.cn 添加新图标
+
+1. **访问项目**:打开 [iconfont.cn](https://www.iconfont.cn/) 的项目页面
+2. **选择图标**:浏览并添加需要的图标到项目
+3. **生成代码**:下载新的字体文件和 CSS 代码
+4. **更新项目**:替换 `iconfont.css` 中的字体链接和图标类名
+
+### 2. 图标命名规范
+
+```css
+/* 建议的命名格式 */
+.icon-[功能]-[状态]
+
+例如:
+.icon-user          /* 用户图标 */
+.icon-eye-open      /* 眼睛打开 */
+.icon-eye-close     /* 眼睛关闭 */
+.icon-arrow-left    /* 左箭头 */
+.icon-arrow-right; /* 右箭头 */
+```
+
+## 📱 演示页面
+
+**页面位置**:`mobile/src/pages/iconfont-demo/index.vue`
+
+访问路径:`/pages/iconfont-demo/index`
+
+演示页面包含:
+
+-   所有可用图标的预览
+-   图标分类展示(基础、操作、导航、联系)
+-   使用代码示例
+-   自定义样式演示
+
+## ⚠️ 注意事项
+
+### 1. 兼容性
+
+-   ✅ **支持平台**:H5、微信小程序、App、支付宝小程序
+-   ✅ **浏览器支持**:Chrome、Safari、Firefox、Edge
+-   ✅ **设备支持**:iOS、Android、HarmonyOS
+
+### 2. 性能优化
+
+```css
+/* 字体预加载 */
+.iconfont {
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+}
+```
+
+### 3. 缓存策略
+
+-   字体文件使用 CDN 加载,自动缓存
+-   本地 CSS 文件确保离线可用
+-   建议定期更新字体链接时间戳
+
+### 4. 回退方案
+
+```vue
+<template>
+    <!-- 图标 + 文字回退 -->
+    <view class="icon-with-fallback">
+        <text class="iconfont icon-user"></text>
+        <text class="icon-fallback">用户</text>
+    </view>
+</template>
+
+<style>
+.icon-fallback {
+    display: none;
+}
+
+/* 当字体加载失败时显示文字 */
+@font-face {
+    font-family: 'iconfont-fallback';
+    src: local('Arial'), local('sans-serif');
+}
+
+.iconfont:before {
+    font-family: 'iconfont', 'iconfont-fallback';
+}
+</style>
+```
+
+## 📚 相关文件
+
+-   **样式文件**:`mobile/src/static/fonts/iconfont.css`
+-   **演示页面**:`mobile/src/pages/iconfont-demo/index.vue`
+-   **登录页面**:`mobile/src/pages/login/index.vue`
+-   **配置文件**:`mobile/src/main.js`
+
+## 🎯 最佳实践
+
+1. **统一使用**:项目中统一使用 iconfont,避免混用多种图标方案
+2. **语义化命名**:使用有意义的类名,便于维护
+3. **适当大小**:根据使用场景选择合适的字体大小
+4. **颜色一致**:遵循设计规范,保持图标颜色的一致性
+5. **性能考虑**:避免一次性加载过多不使用的图标
+
+## 📝 更新日志
+
+-   **2025-07-01**:初始版本,集成 iconfont 字体库
+-   **预计扩展**:根据业务需求持续添加新图标
+
+---
+
+**维护者**:移动端开发团队  
+**更新时间**:2025-07-01

+ 280 - 0
docs/开发文档/移动端路由守卫使用指南.md

@@ -0,0 +1,280 @@
+# 移动端路由守卫使用指南
+
+## 📋 概述
+
+移动端路由守卫是一个全局的权限控制系统,确保用户未登录或登录失效时,访问需要权限的页面会自动跳转到登录页。
+
+## 🎯 核心功能
+
+### 1. 自动权限检查
+
+-   **页面加载时检查** → `onLoad()` 生命周期
+-   **页面显示时检查** → `onShow()` 生命周期
+-   **页面跳转前检查** → 使用守卫跳转方法
+-   **HTTP 请求拦截** → 401 响应自动处理
+
+### 2. 多层次验证
+
+-   ✅ 检查登录状态 `userStorage.isLoggedIn()`
+-   ✅ 检查 Token 存在 `userStorage.getToken()`
+-   ✅ 检查 Token 过期 `userState.checkTokenExpiration()`
+-   ✅ 区分公开页面和受保护页面
+
+### 3. 优雅的用户体验
+
+-   🎨 友好的错误提示
+-   🎨 自动清理失效信息
+-   🎨 延迟跳转确保提示显示
+-   🎨 使用 `reLaunch` 清除页面栈
+
+## 🔧 使用方法
+
+### 在页面中使用
+
+```javascript
+<script>
+import { checkPageAccess } from '@/utils/route-guard';
+
+export default {
+    onLoad() {
+        // 页面加载时检查权限
+        if (!checkPageAccess()) {
+            return; // 权限检查失败,自动跳转登录页
+        }
+
+        // 页面初始化逻辑
+        this.initPageData();
+    },
+
+    onShow() {
+        // 页面显示时再次检查权限(防止后台切换回来时失效)
+        if (!checkPageAccess()) {
+            return;
+        }
+
+        // 刷新页面数据
+        this.refreshData();
+    },
+
+    methods: {
+        initPageData() {
+            // 需要登录权限的初始化逻辑
+        },
+
+        refreshData() {
+            // 需要登录权限的数据刷新逻辑
+        }
+    }
+}
+</script>
+```
+
+### 带权限检查的页面跳转
+
+```javascript
+<script>
+import { guardedNavigate, guardedSwitchTab } from '@/utils/route-guard';
+
+export default {
+    methods: {
+        // 普通页面跳转
+        goToProfile() {
+            guardedNavigate('/pages/profile/index');
+        },
+
+        // Tab页面切换
+        switchToWorkbench() {
+            guardedSwitchTab('/pages/workbench/index');
+        },
+
+        // 带参数跳转
+        goToDetail(id) {
+            guardedNavigate(`/pages/detail/index?id=${id}`);
+        }
+    }
+}
+</script>
+```
+
+### 使用页面混入(可选)
+
+```javascript
+<script>
+import { routeGuardMixin } from '@/utils/route-guard';
+
+export default {
+    // 使用混入,自动添加权限检查
+    mixins: [routeGuardMixin],
+
+    onLoad() {
+        // 混入会自动调用权限检查
+        // 这里只需要写业务逻辑
+        this.loadData();
+    }
+}
+</script>
+```
+
+## ⚙️ 配置说明
+
+### 公开页面配置
+
+在 `mobile/src/utils/route-guard.js` 中配置无需登录的页面:
+
+```javascript
+// 无需登录验证的页面列表
+const PUBLIC_PAGES = [
+    '/pages/login/index', // 登录页
+    '/pages/index/index', // 首页(功能受限但可访问)
+];
+```
+
+### 自定义登录页路径
+
+```javascript
+// 登录页路径
+const LOGIN_PAGE = '/pages/login/index';
+```
+
+## 🧪 测试功能
+
+访问测试页面验证路由守卫功能:
+
+```
+页面路径: /pages/test-route/index
+```
+
+测试页面提供:
+
+-   📊 实时登录状态显示
+-   🔧 测试按钮(清除 Token、跳转等)
+-   📋 详细的操作日志
+-   🎯 权限检查结果验证
+
+## 🔄 工作流程
+
+### 权限检查流程
+
+```mermaid
+graph TD
+    A[页面访问] --> B{是否公开页面?}
+    B -->|是| C[允许访问]
+    B -->|否| D{用户已登录?}
+    D -->|否| E[跳转登录页]
+    D -->|是| F{Token存在?}
+    F -->|否| E
+    F -->|是| G{Token未过期?}
+    G -->|否| E
+    G -->|是| C
+```
+
+### HTTP 拦截流程
+
+```mermaid
+graph TD
+    A[API请求] --> B[发送请求]
+    B --> C{响应状态}
+    C -->|200| D[正常处理]
+    C -->|401| E[清除登录信息]
+    E --> F[显示提示]
+    F --> G[跳转登录页]
+```
+
+## 📝 最佳实践
+
+### 1. 页面权限设计原则
+
+-   **最小权限原则**:只有必要的页面才需要登录
+-   **用户友好原则**:首页等入口页面可以部分访问
+-   **安全优先原则**:敏感操作必须验证权限
+
+### 2. 错误处理建议
+
+```javascript
+onLoad() {
+    try {
+        if (!checkPageAccess()) {
+            return;
+        }
+
+        // 业务逻辑
+        await this.loadUserData();
+
+    } catch (error) {
+        console.error('页面初始化失败:', error);
+        uni.showToast({
+            title: '页面加载失败',
+            icon: 'none'
+        });
+    }
+}
+```
+
+### 3. 性能优化建议
+
+-   权限检查放在业务逻辑之前
+-   避免在权限检查失败后继续执行后续逻辑
+-   合理使用页面缓存减少重复检查
+
+## 🚨 注意事项
+
+### 1. 公开页面处理
+
+```javascript
+// ❌ 错误:在公开页面中强制检查权限
+onLoad() {
+    if (!checkPageAccess()) return; // 首页不应该强制登录
+}
+
+// ✅ 正确:区分功能的权限要求
+onLoad() {
+    // 首页可以访问,但部分功能需要登录
+    this.checkLoginForFeatures();
+}
+```
+
+### 2. 避免循环跳转
+
+```javascript
+// ❌ 错误:在登录页中调用权限检查
+// pages/login/index.vue
+onLoad() {
+    checkPageAccess(); // 会导致循环跳转
+}
+
+// ✅ 正确:登录页无需权限检查
+onLoad() {
+    // 直接执行登录页逻辑
+}
+```
+
+### 3. Token 刷新处理
+
+```javascript
+// 建议在HTTP拦截器中处理Token刷新
+// 避免在页面级别处理复杂的Token逻辑
+```
+
+## 🔗 相关文档
+
+-   [移动端用户管理工具](./移动端用户管理工具使用指南.md)
+-   [移动端 API 请求规范](./移动端API请求规范.md)
+-   [移动端开发规范总结](./移动端开发规范总结.md)
+
+## 💡 常见问题
+
+### Q: 如何添加新的公开页面?
+
+A: 在 `route-guard.js` 的 `PUBLIC_PAGES` 数组中添加页面路径。
+
+### Q: 权限检查失败但没有跳转到登录页?
+
+A: 检查控制台日志,确认 `reLaunch` 是否执行成功,必要时检查页面路径配置。
+
+### Q: 如何自定义权限检查逻辑?
+
+A: 可以扩展 `checkPageAccess` 函数,添加更复杂的权限验证逻辑。
+
+### Q: 在什么情况下需要使用 `routeGuardMixin`?
+
+A: 当页面逻辑复杂,希望自动化权限检查时使用,但通常建议手动调用以保持代码清晰。

+ 15 - 10
mobile/src/App.vue

@@ -1,15 +1,20 @@
 <script>
+import { initRouteGuard } from '@/utils/route-guard';
+
 export default {
-  onLaunch: function () {
-    console.log('App Launch')
-  },
-  onShow: function () {
-    console.log('App Show')
-  },
-  onHide: function () {
-    console.log('App Hide')
-  },
-}
+    onLaunch: function () {
+        console.log('App Launch');
+
+        // 初始化全局路由守卫
+        initRouteGuard();
+    },
+    onShow: function () {
+        console.log('App Show');
+    },
+    onHide: function () {
+        console.log('App Hide');
+    },
+};
 </script>
 
 <style>

+ 32 - 11
mobile/src/main.js

@@ -1,11 +1,32 @@
-import { createSSRApp } from 'vue'
-import App from './App.vue'
-import uviewPlus from 'uview-plus'
-
-export function createApp() {
-  const app = createSSRApp(App)
-  app.use(uviewPlus)
-  return {
-    app
-  }
-}
+import { createSSRApp } from 'vue';
+import App from './App.vue';
+import uviewPlus from 'uview-plus';
+
+// 导入 uview-plus 样式
+import 'uview-plus/index.scss';
+
+// 导入 iconfont 样式
+import '@/static/fonts/iconfont.css';
+
+// 预加载字体文件 - 使用阿里CDN
+uni.loadFontFace({
+    family: 'uicon-iconfont',
+    source: 'url("https://at.alicdn.com/t/font_2225171_8kdcwk4po24.ttf")',
+    success() {
+        console.log('uview-plus图标字体加载成功');
+    },
+    fail(error) {
+        console.error('uview-plus图标字体加载失败', error);
+    },
+});
+
+export function createApp() {
+    const app = createSSRApp(App);
+
+    // 注册 uview-plus
+    app.use(uviewPlus);
+
+    return {
+        app,
+    };
+}

+ 32 - 0
mobile/src/pages.json

@@ -1,5 +1,22 @@
 {
+	"easycom": {
+		"autoscan": true,
+		"custom": {
+			"^u-(.*)": "uview-plus/components/u-$1/u-$1.vue",
+			"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
+			"^uv-(.*)": "uview-plus/components/u-$1/u-$1.vue"
+		}
+	},
 	"pages": [
+		{
+			"path": "pages/login/index",
+			"style": {
+				"navigationBarTitleText": "登录",
+				"navigationStyle": "custom",
+				"navigationBarBackgroundColor": "transparent",
+				"backgroundColor": "transparent"
+			}
+		},
 		{
 			"path": "pages/index/index",
 			"style": {
@@ -20,6 +37,21 @@
 				"navigationBarTitleText": "我的",
 				"navigationStyle": "custom"
 			}
+		},
+		{
+			"path": "pages/iconfont-demo/index",
+			"style": {
+				"navigationBarTitleText": "图标演示",
+				"navigationBarBackgroundColor": "#5d87ff",
+				"navigationBarTextStyle": "white"
+			}
+		},
+		{
+			"path": "pages/test-route/index",
+			"style": {
+				"navigationBarTitleText": "路由守卫测试",
+				"navigationStyle": "custom"
+			}
 		}
 	],
 	"globalStyle": {

+ 262 - 0
mobile/src/pages/iconfont-demo/index.vue

@@ -0,0 +1,262 @@
+<template>
+    <view class="iconfont-demo">
+        <view class="demo-header">
+            <text class="demo-title">iconfont 图标库演示</text>
+            <text class="demo-subtitle">基于iconfont.cn的图标字体</text>
+        </view>
+
+        <view class="demo-content">
+            <!-- 基础图标 -->
+            <view class="icon-section">
+                <text class="section-title">基础图标</text>
+                <view class="icon-grid">
+                    <view class="icon-item">
+                        <text class="iconfont icon-user"></text>
+                        <text class="icon-name">用户</text>
+                        <text class="icon-code">icon-user</text>
+                    </view>
+                    <view class="icon-item">
+                        <text class="iconfont icon-lock"></text>
+                        <text class="icon-name">锁定</text>
+                        <text class="icon-code">icon-lock</text>
+                    </view>
+                    <view class="icon-item">
+                        <text class="iconfont icon-eye"></text>
+                        <text class="icon-name">眼睛</text>
+                        <text class="icon-code">icon-eye</text>
+                    </view>
+                    <view class="icon-item">
+                        <text class="iconfont icon-eye-close"></text>
+                        <text class="icon-name">关闭眼睛</text>
+                        <text class="icon-code">icon-eye-close</text>
+                    </view>
+                </view>
+            </view>
+
+            <!-- 操作图标 -->
+            <view class="icon-section">
+                <text class="section-title">操作图标</text>
+                <view class="icon-grid">
+                    <view class="icon-item">
+                        <text class="iconfont icon-login"></text>
+                        <text class="icon-name">登录</text>
+                        <text class="icon-code">icon-login</text>
+                    </view>
+                    <view class="icon-item">
+                        <text class="iconfont icon-search"></text>
+                        <text class="icon-name">搜索</text>
+                        <text class="icon-code">icon-search</text>
+                    </view>
+                    <view class="icon-item">
+                        <text class="iconfont icon-add"></text>
+                        <text class="icon-name">添加</text>
+                        <text class="icon-code">icon-add</text>
+                    </view>
+                    <view class="icon-item">
+                        <text class="iconfont icon-close"></text>
+                        <text class="icon-name">关闭</text>
+                        <text class="icon-code">icon-close</text>
+                    </view>
+                </view>
+            </view>
+
+            <!-- 导航图标 -->
+            <view class="icon-section">
+                <text class="section-title">导航图标</text>
+                <view class="icon-grid">
+                    <view class="icon-item">
+                        <text class="iconfont icon-home"></text>
+                        <text class="icon-name">首页</text>
+                        <text class="icon-code">icon-home</text>
+                    </view>
+                    <view class="icon-item">
+                        <text class="iconfont icon-message"></text>
+                        <text class="icon-name">消息</text>
+                        <text class="icon-code">icon-message</text>
+                    </view>
+                    <view class="icon-item">
+                        <text class="iconfont icon-setting"></text>
+                        <text class="icon-name">设置</text>
+                        <text class="icon-code">icon-setting</text>
+                    </view>
+                    <view class="icon-item">
+                        <text class="iconfont icon-arrow-right"></text>
+                        <text class="icon-name">箭头右</text>
+                        <text class="icon-code">icon-arrow-right</text>
+                    </view>
+                </view>
+            </view>
+
+            <!-- 联系图标 -->
+            <view class="icon-section">
+                <text class="section-title">联系图标</text>
+                <view class="icon-grid">
+                    <view class="icon-item">
+                        <text class="iconfont icon-phone"></text>
+                        <text class="icon-name">电话</text>
+                        <text class="icon-code">icon-phone</text>
+                    </view>
+                    <view class="icon-item">
+                        <text class="iconfont icon-mail"></text>
+                        <text class="icon-name">邮箱</text>
+                        <text class="icon-code">icon-mail</text>
+                    </view>
+                    <view class="icon-item">
+                        <text class="iconfont icon-check"></text>
+                        <text class="icon-name">确认</text>
+                        <text class="icon-code">icon-check</text>
+                    </view>
+                </view>
+            </view>
+
+            <!-- 使用说明 -->
+            <view class="usage-section">
+                <text class="section-title">使用方法</text>
+                <view class="usage-code">
+                    <text class="code-line">
+                        &lt;text class="iconfont icon-user"&gt;&lt;/text&gt;
+                    </text>
+                    <text class="code-line">
+                        &lt;text class="iconfont icon-lock"&gt;&lt;/text&gt;
+                    </text>
+                    <text class="code-line">/* 自定义样式 */</text>
+                    <text class="code-line">.custom-icon {</text>
+                    <text class="code-line">font-size: 24px;</text>
+                    <text class="code-line">color: #5d87ff;</text>
+                    <text class="code-line">}</text>
+                </view>
+            </view>
+        </view>
+    </view>
+</template>
+
+<script>
+export default {
+    name: 'IconFontDemo',
+    data() {
+        return {};
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+.iconfont-demo {
+    min-height: 100vh;
+    background: #f8f9fa;
+    padding: 20px;
+}
+
+.demo-header {
+    text-align: center;
+    margin-bottom: 32px;
+}
+
+.demo-title {
+    display: block;
+    font-size: 24px;
+    font-weight: bold;
+    color: #222;
+    margin-bottom: 8px;
+}
+
+.demo-subtitle {
+    display: block;
+    font-size: 14px;
+    color: #666;
+}
+
+.demo-content {
+    max-width: 800px;
+    margin: 0 auto;
+}
+
+.icon-section {
+    background: #fff;
+    border-radius: 12px;
+    padding: 20px;
+    margin-bottom: 20px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.section-title {
+    display: block;
+    font-size: 18px;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 16px;
+    padding-bottom: 8px;
+    border-bottom: 2px solid #5d87ff;
+}
+
+.icon-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+    gap: 16px;
+}
+
+.icon-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 16px 8px;
+    border: 1px solid #e9ecef;
+    border-radius: 8px;
+    background: #fafbfc;
+    transition: all 0.2s ease;
+}
+
+.icon-item:hover {
+    border-color: #5d87ff;
+    background: #f0f4ff;
+}
+
+.icon-item .iconfont {
+    width: 32px;
+    height: 32px;
+    margin-bottom: 8px;
+    filter: brightness(0) saturate(100%) invert(38%) sepia(87%) saturate(6400%) hue-rotate(225deg)
+        brightness(104%) contrast(104%);
+}
+
+.icon-name {
+    font-size: 14px;
+    color: #333;
+    margin-bottom: 4px;
+    font-weight: 500;
+}
+
+.icon-code {
+    font-size: 12px;
+    color: #666;
+    font-family: 'Consolas', 'Monaco', monospace;
+    background: #f1f3f4;
+    padding: 2px 6px;
+    border-radius: 4px;
+}
+
+.usage-section {
+    background: #fff;
+    border-radius: 12px;
+    padding: 20px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.usage-code {
+    background: #2d3748;
+    border-radius: 8px;
+    padding: 16px;
+    font-family: 'Consolas', 'Monaco', monospace;
+}
+
+.code-line {
+    display: block;
+    color: #e2e8f0;
+    font-size: 14px;
+    line-height: 1.6;
+    margin-bottom: 4px;
+}
+
+.code-line:last-child {
+    margin-bottom: 0;
+}
+</style>

+ 26 - 2
mobile/src/pages/index/index.vue

@@ -166,6 +166,9 @@ import Card from '@/components/Card.vue';
 import ServiceItem from '@/components/ServiceItem.vue';
 import StatCard from '@/components/StatCard.vue';
 import GridLayout from '@/components/GridLayout.vue';
+import { userStorage } from '@/utils/request';
+import { userManager, userState } from '@/utils/user';
+import { guardedSwitchTab } from '@/utils/route-guard';
 
 export default {
     name: 'HomePage',
@@ -179,7 +182,7 @@ export default {
     data() {
         return {
             userInfo: {
-                name: '张同学',
+                name: '用户',
             },
             currentDateTime: '',
             weather: {
@@ -279,6 +282,7 @@ export default {
         };
     },
     onLoad() {
+        this.checkLoginStatus();
         this.updateDateTime();
         this.startTimeUpdate();
     },
@@ -288,6 +292,26 @@ export default {
         }
     },
     methods: {
+        // 检查登录状态
+        checkLoginStatus() {
+            // 使用新的状态检查工具
+            if (!userState.autoLoginCheck()) {
+                return;
+            }
+
+            // 获取并更新用户信息
+            this.loadUserInfo();
+        },
+
+        // 加载用户信息
+        loadUserInfo() {
+            const displayUser = userManager.formatUserForDisplay();
+            if (displayUser) {
+                this.userInfo.name = displayUser.name;
+                this.userInfo.avatar = displayUser.avatar;
+            }
+        },
+
         updateDateTime() {
             const now = new Date();
             const month = now.getMonth() + 1;
@@ -327,7 +351,7 @@ export default {
             uni.showToast({ title: `使用${tool.title}`, icon: 'none' });
         },
         navigateToService() {
-            uni.switchTab({ url: '/pages/workbench/index' });
+            guardedSwitchTab('/pages/workbench/index');
         },
         viewAllTodos() {
             uni.showToast({ title: '查看全部待办', icon: 'none' });

+ 1 - 0
mobile/src/pages/login/README.md

@@ -0,0 +1 @@
+ 

+ 65 - 11
mobile/src/pages/profile/index.vue

@@ -155,6 +155,9 @@ import NavBar from '@/components/NavBar.vue';
 import Card from '@/components/Card.vue';
 import StatCard from '@/components/StatCard.vue';
 import GridLayout from '@/components/GridLayout.vue';
+import { userStorage } from '@/utils/request';
+import { userManager, userState } from '@/utils/user';
+import { checkPageAccess } from '@/utils/route-guard';
 
 export default {
     name: 'ProfilePage',
@@ -167,13 +170,13 @@ export default {
     data() {
         return {
             userInfo: {
-                name: '张同学',
-                major: '计算机科学与技术',
-                grade: '大三',
-                studentId: '2021001234',
-                gpa: 4.2,
-                credits: 95,
-                ranking: 15,
+                name: '用户',
+                major: '未设置',
+                grade: '未设置',
+                studentId: '未设置',
+                gpa: 0,
+                credits: 0,
+                ranking: 0,
             },
             learningStats: [
                 { number: 5, label: '本周课程' },
@@ -288,6 +291,24 @@ export default {
         };
     },
     methods: {
+        // 检查登录状态
+        checkLoginStatus() {
+            return userState.autoLoginCheck();
+        },
+        
+        // 加载用户信息
+        loadUserInfo() {
+            const displayUser = userManager.formatUserForDisplay();
+            if (displayUser) {
+                this.userInfo.name = displayUser.name;
+                this.userInfo.avatar = displayUser.avatar;
+                this.userInfo.department = displayUser.department;
+                this.userInfo.phone = displayUser.phone;
+                this.userInfo.email = displayUser.email;
+                this.userInfo.studentId = userManager.getCurrentUser()?.userId || '未设置';
+            }
+        },
+        
         handleSettings() {
             uni.showToast({ title: '设置', icon: 'none' });
         },
@@ -344,17 +365,50 @@ export default {
                 content: '确定要退出登录吗?',
                 success: (res) => {
                     if (res.confirm) {
-                        uni.showToast({ title: '正在退出...', icon: 'loading' });
+                        uni.showLoading({ title: '正在退出...' });
+                        
+                        // 清除登录信息
+                        userStorage.clearLoginInfo();
+                        
                         setTimeout(() => {
-                            uni.showToast({ title: '已退出登录', icon: 'success' });
-                            // 这里可以跳转到登录页面
+                            uni.hideLoading();
+                            uni.showToast({ 
+                                title: '已退出登录', 
+                                icon: 'success' 
+                            });
+                            
+                            // 跳转到登录页面
+                            setTimeout(() => {
+                                uni.reLaunch({
+                                    url: '/pages/login/index'
+                                });
+                            }, 500);
                         }, 1000);
                     }
                 },
             });
         },
     },
-    onLoad() {},
+    onLoad() {
+        // 检查页面访问权限
+        if (!checkPageAccess()) {
+            return;
+        }
+        
+        if (this.checkLoginStatus()) {
+            this.loadUserInfo();
+        }
+    },
+    
+    onShow() {
+        // 页面显示时再次检查权限
+        if (!checkPageAccess()) {
+            return;
+        }
+        
+        // 刷新用户信息
+        this.loadUserInfo();
+    },
 };
 </script>
 

+ 346 - 0
mobile/src/pages/test-route/index.vue

@@ -0,0 +1,346 @@
+<template>
+    <view class="test-page">
+        <NavBar title="路由守卫测试" />
+
+        <view class="content">
+            <view class="test-section">
+                <text class="section-title">🛡️ 路由守卫测试页面</text>
+                <text class="section-desc">如果你能看到这个页面,说明已经通过了登录验证</text>
+            </view>
+
+            <view class="test-info">
+                <view class="info-item">
+                    <text class="info-label">当前时间:</text>
+                    <text class="info-value">{{ currentTime }}</text>
+                </view>
+                <view class="info-item">
+                    <text class="info-label">登录状态:</text>
+                    <text class="info-value" :class="isLoggedIn ? 'status-ok' : 'status-error'">
+                        {{ isLoggedIn ? '✅ 已登录' : '❌ 未登录' }}
+                    </text>
+                </view>
+                <view class="info-item">
+                    <text class="info-label">Token状态:</text>
+                    <text class="info-value" :class="hasToken ? 'status-ok' : 'status-error'">
+                        {{ hasToken ? '✅ 有效' : '❌ 无效' }}
+                    </text>
+                </view>
+                <view class="info-item">
+                    <text class="info-label">用户信息:</text>
+                    <text class="info-value">{{ userName || '未获取' }}</text>
+                </view>
+            </view>
+
+            <view class="test-buttons">
+                <button class="test-btn" @click="testRedirectToLogin">测试跳转登录页</button>
+                <button class="test-btn" @click="testClearToken">清除Token测试</button>
+                <button class="test-btn" @click="testNavigateWithGuard">带守卫的页面跳转</button>
+                <button class="test-btn" @click="refreshPageInfo">刷新页面信息</button>
+            </view>
+
+            <view class="log-section">
+                <text class="section-title">📋 测试日志</text>
+                <scroll-view class="log-content" scroll-y>
+                    <view
+                        v-for="(log, index) in logs"
+                        :key="index"
+                        class="log-item"
+                        :class="log.type"
+                    >
+                        <text class="log-time">{{ log.time }}</text>
+                        <text class="log-text">{{ log.message }}</text>
+                    </view>
+                </scroll-view>
+            </view>
+        </view>
+    </view>
+</template>
+
+<script>
+import NavBar from '@/components/NavBar.vue';
+import { userStorage } from '@/utils/request';
+import { userManager, userState } from '@/utils/user';
+import { checkPageAccess, guardedNavigate } from '@/utils/route-guard';
+
+export default {
+    name: 'TestRoutePage',
+    components: {
+        NavBar,
+    },
+    data() {
+        return {
+            currentTime: '',
+            isLoggedIn: false,
+            hasToken: false,
+            userName: '',
+            logs: [],
+        };
+    },
+    onLoad() {
+        this.addLog('info', '页面加载开始');
+
+        // 检查页面访问权限
+        if (!checkPageAccess()) {
+            this.addLog('error', '页面访问权限检查失败,应该会自动跳转到登录页');
+            return;
+        }
+
+        this.addLog('success', '页面访问权限检查通过');
+        this.refreshPageInfo();
+    },
+
+    onShow() {
+        this.addLog('info', '页面显示事件触发');
+
+        // 页面显示时再次检查权限
+        if (!checkPageAccess()) {
+            this.addLog('error', '页面显示时权限检查失败');
+            return;
+        }
+
+        this.addLog('success', '页面显示时权限检查通过');
+        this.updateCurrentTime();
+    },
+
+    onUnload() {
+        this.addLog('info', '页面卸载');
+    },
+
+    methods: {
+        // 刷新页面信息
+        refreshPageInfo() {
+            this.updateCurrentTime();
+            this.checkLoginStatus();
+            this.checkTokenStatus();
+            this.loadUserInfo();
+            this.addLog('info', '页面信息已刷新');
+        },
+
+        // 更新当前时间
+        updateCurrentTime() {
+            this.currentTime = new Date().toLocaleString();
+        },
+
+        // 检查登录状态
+        checkLoginStatus() {
+            this.isLoggedIn = userStorage.isLoggedIn();
+            this.addLog('info', `登录状态检查: ${this.isLoggedIn ? '已登录' : '未登录'}`);
+        },
+
+        // 检查Token状态
+        checkTokenStatus() {
+            const token = userStorage.getToken();
+            this.hasToken = !!token;
+            const isExpired = userState.checkTokenExpiration();
+
+            this.addLog('info', `Token检查: ${this.hasToken ? '存在' : '不存在'}`);
+            if (this.hasToken) {
+                this.addLog('info', `Token过期检查: ${isExpired ? '已过期' : '未过期'}`);
+            }
+        },
+
+        // 加载用户信息
+        loadUserInfo() {
+            const user = userManager.getCurrentUser();
+            this.userName = user ? user.nickName || user.userName : '';
+            this.addLog('info', `用户信息: ${this.userName || '未获取到'}`);
+        },
+
+        // 测试跳转到登录页
+        testRedirectToLogin() {
+            this.addLog('warning', '手动跳转到登录页测试');
+            uni.reLaunch({
+                url: '/pages/login/index',
+            });
+        },
+
+        // 测试清除Token
+        testClearToken() {
+            this.addLog('warning', '清除Token进行测试');
+            userStorage.clearLoginInfo();
+            this.addLog('error', 'Token已清除,页面应该会自动跳转到登录页');
+
+            // 模拟页面重新检查权限
+            setTimeout(() => {
+                if (!checkPageAccess()) {
+                    this.addLog('success', '权限检查失败,正在跳转登录页');
+                }
+            }, 1000);
+        },
+
+        // 测试带守卫的页面跳转
+        testNavigateWithGuard() {
+            this.addLog('info', '测试带权限检查的页面跳转');
+            const result = guardedNavigate('/pages/profile/index');
+
+            if (result) {
+                this.addLog('success', '跳转成功');
+            } else {
+                this.addLog('error', '跳转失败或被拦截');
+            }
+        },
+
+        // 添加日志
+        addLog(type, message) {
+            const log = {
+                type,
+                time: new Date().toLocaleTimeString(),
+                message,
+            };
+
+            this.logs.unshift(log);
+
+            // 限制日志数量
+            if (this.logs.length > 20) {
+                this.logs.pop();
+            }
+
+            console.log(`[${type.toUpperCase()}] ${message}`);
+        },
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+.test-page {
+    min-height: 100vh;
+    background-color: $uni-bg-color-grey;
+}
+
+.content {
+    padding: 16px;
+}
+
+.test-section {
+    background: $uni-bg-color;
+    border-radius: $uni-border-radius-lg;
+    padding: 20px;
+    margin-bottom: 16px;
+    text-align: center;
+}
+
+.section-title {
+    font-size: 18px;
+    font-weight: 600;
+    color: $uni-text-color;
+    display: block;
+    margin-bottom: 8px;
+}
+
+.section-desc {
+    font-size: 14px;
+    color: $uni-text-color-grey;
+    line-height: 1.5;
+}
+
+.test-info {
+    background: $uni-bg-color;
+    border-radius: $uni-border-radius-lg;
+    padding: 16px;
+    margin-bottom: 16px;
+}
+
+.info-item {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 8px 0;
+    border-bottom: 1px solid #f0f0f0;
+
+    &:last-child {
+        border-bottom: none;
+    }
+}
+
+.info-label {
+    font-size: 14px;
+    color: $uni-text-color-grey;
+}
+
+.info-value {
+    font-size: 14px;
+    color: $uni-text-color;
+    font-weight: 500;
+
+    &.status-ok {
+        color: #52c41a;
+    }
+
+    &.status-error {
+        color: #ff4d4f;
+    }
+}
+
+.test-buttons {
+    margin-bottom: 16px;
+}
+
+.test-btn {
+    width: 100%;
+    background: $uni-color-primary;
+    color: $uni-text-color-inverse;
+    border: none;
+    border-radius: $uni-border-radius-base;
+    padding: 12px;
+    margin-bottom: 8px;
+    font-size: 14px;
+
+    &:last-child {
+        margin-bottom: 0;
+    }
+
+    &:active {
+        background: $uni-color-primary-dark;
+    }
+}
+
+.log-section {
+    background: $uni-bg-color;
+    border-radius: $uni-border-radius-lg;
+    padding: 16px;
+}
+
+.log-content {
+    max-height: 300px;
+    margin-top: 12px;
+}
+
+.log-item {
+    padding: 8px 12px;
+    margin-bottom: 4px;
+    border-radius: $uni-border-radius-base;
+    border-left: 3px solid;
+
+    &.info {
+        background: #f0f9ff;
+        border-left-color: #0ea5e9;
+    }
+
+    &.success {
+        background: #f0f9f4;
+        border-left-color: #22c55e;
+    }
+
+    &.warning {
+        background: #fffbeb;
+        border-left-color: #f59e0b;
+    }
+
+    &.error {
+        background: #fef2f2;
+        border-left-color: #ef4444;
+    }
+}
+
+.log-time {
+    font-size: 12px;
+    color: $uni-text-color-grey;
+    margin-right: 8px;
+}
+
+.log-text {
+    font-size: 13px;
+    color: $uni-text-color;
+    line-height: 1.4;
+}
+</style>

+ 26 - 1
mobile/src/pages/workbench/index.vue

@@ -165,6 +165,8 @@ import NavBar from '@/components/NavBar.vue';
 import Card from '@/components/Card.vue';
 import ServiceItem from '@/components/ServiceItem.vue';
 import GridLayout from '@/components/GridLayout.vue';
+import { userStorage } from '@/utils/request';
+import { checkPageAccess } from '@/utils/route-guard';
 
 export default {
     name: 'WorkbenchPage',
@@ -340,8 +342,31 @@ export default {
                 },
             });
         },
+        // 检查登录状态
+        checkLoginStatus() {
+            if (!userStorage.isLoggedIn()) {
+                uni.reLaunch({
+                    url: '/pages/login/index',
+                });
+                return;
+            }
+        },
+    },
+    onLoad() {
+        // 检查页面访问权限
+        if (!checkPageAccess()) {
+            return;
+        }
+        
+        this.checkLoginStatus();
+    },
+    
+    onShow() {
+        // 页面显示时再次检查权限
+        if (!checkPageAccess()) {
+            return;
+        }
     },
-    onLoad() {},
 };
 </script>
 

BIN
mobile/src/static/fonts/font_2225171_8kdcwk4po24.ttf


+ 75 - 1
mobile/src/static/fonts/iconfont.css

@@ -1 +1,75 @@
-/* 空的iconfont文件 - 用于解决依赖问题 */
+.iconfont {
+  display: inline-block;
+  width: 16px;
+  height: 16px;
+  background-size: contain;
+  background-repeat: no-repeat;
+  background-position: center;
+}
+
+/* 用户图标 */
+.icon-user {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z'/%3E%3C/svg%3E");
+}
+
+/* 密码锁图标 */
+.icon-lock {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6z'/%3E%3C/svg%3E");
+}
+
+/* 眼睛图标(显示密码) */
+.icon-eye {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z'/%3E%3C/svg%3E");
+}
+
+/* 眼睛关闭图标(隐藏密码) */
+.icon-eye-close {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z'/%3E%3C/svg%3E");
+}
+
+/* 登录图标 */
+.icon-login {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M11 7L9.6 8.4l2.6 2.6H2v2h10.2l-2.6 2.6L11 17l5-5-5-5zm9 12h-8v2h8c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-8v2h8v14z'/%3E%3C/svg%3E");
+}
+
+/* 箭头右图标 */
+.icon-arrow-right {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'/%3E%3C/svg%3E");
+}
+
+/* 更多常用图标 */
+.icon-home {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z'/%3E%3C/svg%3E");
+}
+
+.icon-message {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z'/%3E%3C/svg%3E");
+}
+
+.icon-setting {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z'/%3E%3C/svg%3E");
+}
+
+.icon-search {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3C/svg%3E");
+}
+
+.icon-add {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z'/%3E%3C/svg%3E");
+}
+
+.icon-check {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E");
+}
+
+.icon-close {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E");
+}
+
+.icon-phone {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z'/%3E%3C/svg%3E");
+}
+
+.icon-mail {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z'/%3E%3C/svg%3E");
+}

BIN
mobile/src/static/logo-01.jpg


BIN
mobile/src/static/logo-2.png


+ 308 - 302
mobile/src/uni.scss

@@ -1,303 +1,309 @@
-/**
- * 这里是uni-app内置的常用样式变量
- *
- * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
- * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
- *
- */
-
-/**
- * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
- *
- * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
- */
-
-// 引入 uview-plus 样式
-@import 'uview-plus/theme.scss';
-
-/* 原有的Sass变量定义 - 保持向后兼容 */
-$uni-color-primary: rgb(93, 135, 255);
-$uni-color-primary-dark: rgb(73, 115, 235);
-$uni-color-primary-light: rgba(93, 135, 255, 0.1);
-
-/* 状态颜色 */
-$uni-color-success: #52c41a;
-$uni-color-warning: #faad14;
-$uni-color-error: #ff4d4f;
-$uni-color-info: #1890ff;
-
-/* 文字基本颜色 */
-$uni-text-color: #333333;
-$uni-text-color-inverse: #fff;
-$uni-text-color-secondary: #666666;
-$uni-text-color-grey: #999999;
-$uni-text-color-placeholder: #c0c0c0;
-$uni-text-color-disable: #d9d9d9;
-
-/* 背景颜色 */
-$uni-bg-color: #ffffff;
-$uni-bg-color-grey: #f8f9fa;
-$uni-bg-color-hover: #f1f1f1;
-$uni-bg-color-mask: rgba(0, 0, 0, 0.4);
-
-/* 边框颜色 */
-$uni-border-color: #e0e0e0;
-
-/* 阴影 */
-$uni-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
-$uni-shadow-hover: 0 4px 20px rgba(0, 0, 0, 0.15);
-
-/* 文字尺寸 */
-$uni-font-size-sm: 12px;
-$uni-font-size-base: 14px;
-$uni-font-size-lg: 16px;
-$uni-font-size-xl: 18px;
-$uni-font-size-xxl: 20px;
-
-/* 图片尺寸 */
-$uni-img-size-sm: 20px;
-$uni-img-size-base: 26px;
-$uni-img-size-lg: 40px;
-
-/* Border Radius */
-$uni-border-radius-sm: 6px;
-$uni-border-radius-base: 8px;
-$uni-border-radius-lg: 12px;
-$uni-border-radius-xl: 16px;
-$uni-border-radius-circle: 50%;
-
-/* 间距 */
-$uni-spacing-row-sm: 8px;
-$uni-spacing-row-base: 12px;
-$uni-spacing-row-lg: 16px;
-$uni-spacing-row-xl: 20px;
-
-$uni-spacing-col-sm: 8px;
-$uni-spacing-col-base: 12px;
-$uni-spacing-col-lg: 16px;
-$uni-spacing-col-xl: 20px;
-
-/* 透明度 */
-$uni-opacity-disabled: 0.3;
-
-/* 文章场景相关 */
-$uni-color-title: #2c405a;
-$uni-font-size-title: 20px;
-$uni-color-subtitle: #555;
-$uni-font-size-subtitle: 18px;
-$uni-color-paragraph: #3f536e;
-$uni-font-size-paragraph: 15px;
-
-/* 动画 */
-$uni-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-$uni-transition-fast: all 0.15s ease-out;
-
-/* 主题色彩变量 - CSS变量版本 */
-:root {
-  --primary-color: rgb(93, 135, 255);
-  --primary-light: rgba(93, 135, 255, 0.1);
-  --primary-dark: rgb(73, 115, 235);
-  --success-color: #52c41a;
-  --warning-color: #faad14;
-  --error-color: #ff4d4f;
-  --text-primary: #262626;
-  --text-secondary: #595959;
-  --text-disabled: #bfbfbf;
-  --border-color: #f0f0f0;
-  --background-color: #f8f9fa;
-  --white: #ffffff;
-  --shadow-light: 0 2px 8px rgba(0, 0, 0, 0.06);
-  --shadow-medium: 0 4px 16px rgba(0, 0, 0, 0.12);
-  --radius-small: 4px;
-  --radius-medium: 8px;
-  --radius-large: 12px;
-  --spacing-xs: 4px;
-  --spacing-sm: 8px;
-  --spacing-md: 16px;
-  --spacing-lg: 24px;
-  --spacing-xl: 32px;
-  --transition-fast: 0.2s ease;
-  --transition-medium: 0.3s ease;
-  --font-size-xs: 12px;
-  --font-size-sm: 14px;
-  --font-size-md: 16px;
-  --font-size-lg: 18px;
-  --font-size-xl: 20px;
-}
-
-/* 全局样式重置 */
-* {
-  box-sizing: border-box;
-  margin: 0;
-  padding: 0;
-}
-
-body {
-  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-  color: var(--text-primary);
-  background-color: var(--background-color);
-  line-height: 1.6;
-}
-
-page {
-  background-color: var(--background-color);
-  color: var(--text-primary);
-  font-size: var(--font-size-md);
-  line-height: 1.6;
-}
-
-/* 通用工具类 */
-.flex {
-  display: flex;
-}
-
-.flex-center {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.flex-between {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-}
-
-.flex-column {
-  display: flex;
-  flex-direction: column;
-}
-
-.text-center {
-  text-align: center;
-}
-
-.text-primary {
-  color: var(--primary-color);
-}
-
-.text-secondary {
-  color: var(--text-secondary);
-}
-
-.bg-primary {
-  background-color: var(--primary-color);
-}
-
-.bg-white {
-  background-color: var(--white);
-}
-
-.rounded {
-  border-radius: var(--radius-medium);
-}
-
-.shadow {
-  box-shadow: var(--shadow-light);
-}
-
-.mt-sm { margin-top: var(--spacing-sm); }
-.mt-md { margin-top: var(--spacing-md); }
-.mt-lg { margin-top: var(--spacing-lg); }
-.mb-sm { margin-bottom: var(--spacing-sm); }
-.mb-md { margin-bottom: var(--spacing-md); }
-.mb-lg { margin-bottom: var(--spacing-lg); }
-.ml-sm { margin-left: var(--spacing-sm); }
-.ml-md { margin-left: var(--spacing-md); }
-.mr-sm { margin-right: var(--spacing-sm); }
-.mr-md { margin-right: var(--spacing-md); }
-
-.p-sm { padding: var(--spacing-sm); }
-.p-md { padding: var(--spacing-md); }
-.p-lg { padding: var(--spacing-lg); }
-.px-sm { padding-left: var(--spacing-sm); padding-right: var(--spacing-sm); }
-.px-md { padding-left: var(--spacing-md); padding-right: var(--spacing-md); }
-.py-sm { padding-top: var(--spacing-sm); padding-bottom: var(--spacing-sm); }
-.py-md { padding-top: var(--spacing-md); padding-bottom: var(--spacing-md); }
-
-/* 按钮基础样式 */
-.btn {
-  padding: var(--spacing-sm) var(--spacing-md);
-  border-radius: var(--radius-medium);
-  border: none;
-  font-size: var(--font-size-md);
-  cursor: pointer;
-  transition: var(--transition-fast);
-  display: inline-flex;
-  align-items: center;
-  justify-content: center;
-  text-decoration: none;
-}
-
-.btn-primary {
-  background-color: var(--primary-color);
-  color: var(--white);
-}
-
-.btn-primary:hover {
-  background-color: var(--primary-dark);
-}
-
-/* 输入框样式 */
-.input {
-  padding: var(--spacing-sm) var(--spacing-md);
-  border: 1px solid var(--border-color);
-  border-radius: var(--radius-medium);
-  font-size: var(--font-size-md);
-  transition: var(--transition-fast);
-}
-
-.input:focus {
-  border-color: var(--primary-color);
-  outline: none;
-  box-shadow: 0 0 0 2px rgba(93, 135, 255, 0.2);
-}
-
-/* 卡片样式 */
-.card {
-  background: var(--white);
-  border-radius: var(--radius-large);
-  box-shadow: var(--shadow-light);
-  padding: var(--spacing-md);
-  margin-bottom: var(--spacing-md);
-}
-
-.card-header {
-  font-size: var(--font-size-lg);
-  font-weight: 600;
-  margin-bottom: var(--spacing-md);
-  color: var(--text-primary);
-}
-
-/* 动画类 */
-.fade-in {
-  animation: fadeIn var(--transition-medium);
-}
-
-.slide-up {
-  animation: slideUp var(--transition-medium);
-}
-
-@keyframes fadeIn {
-  from { opacity: 0; }
-  to { opacity: 1; }
-}
-
-@keyframes slideUp {
-  from { 
-    opacity: 0; 
-    transform: translateY(20px); 
-  }
-  to { 
-    opacity: 1; 
-    transform: translateY(0); 
-  }
-}
-
-/* 响应式断点 */
-@media (max-width: 768px) {
-  .card {
-    margin: var(--spacing-sm);
-    padding: var(--spacing-sm);
-  }
+/**
+ * 这里是uni-app内置的常用样式变量
+ *
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ *
+ */
+
+/**
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ *
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+ */
+
+// 引入 uview-plus 样式
+@import 'uview-plus/theme.scss';
+
+// 引入 uview-plus 图标字体 - 使用阿里CDN
+@font-face {
+  font-family: 'uicon-iconfont';
+  src: url('https://at.alicdn.com/t/font_2225171_8kdcwk4po24.ttf') format('truetype');
+}
+
+/* 原有的Sass变量定义 - 保持向后兼容 */
+$uni-color-primary: rgb(93, 135, 255);
+$uni-color-primary-dark: rgb(73, 115, 235);
+$uni-color-primary-light: rgba(93, 135, 255, 0.1);
+
+/* 状态颜色 */
+$uni-color-success: #52c41a;
+$uni-color-warning: #faad14;
+$uni-color-error: #ff4d4f;
+$uni-color-info: #1890ff;
+
+/* 文字基本颜色 */
+$uni-text-color: #333333;
+$uni-text-color-inverse: #fff;
+$uni-text-color-secondary: #666666;
+$uni-text-color-grey: #999999;
+$uni-text-color-placeholder: #c0c0c0;
+$uni-text-color-disable: #d9d9d9;
+
+/* 背景颜色 */
+$uni-bg-color: #ffffff;
+$uni-bg-color-grey: #f8f9fa;
+$uni-bg-color-hover: #f1f1f1;
+$uni-bg-color-mask: rgba(0, 0, 0, 0.4);
+
+/* 边框颜色 */
+$uni-border-color: #e0e0e0;
+
+/* 阴影 */
+$uni-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+$uni-shadow-hover: 0 4px 20px rgba(0, 0, 0, 0.15);
+
+/* 文字尺寸 */
+$uni-font-size-sm: 12px;
+$uni-font-size-base: 14px;
+$uni-font-size-lg: 16px;
+$uni-font-size-xl: 18px;
+$uni-font-size-xxl: 20px;
+
+/* 图片尺寸 */
+$uni-img-size-sm: 20px;
+$uni-img-size-base: 26px;
+$uni-img-size-lg: 40px;
+
+/* Border Radius */
+$uni-border-radius-sm: 6px;
+$uni-border-radius-base: 8px;
+$uni-border-radius-lg: 12px;
+$uni-border-radius-xl: 16px;
+$uni-border-radius-circle: 50%;
+
+/* 间距 */
+$uni-spacing-row-sm: 8px;
+$uni-spacing-row-base: 12px;
+$uni-spacing-row-lg: 16px;
+$uni-spacing-row-xl: 20px;
+
+$uni-spacing-col-sm: 8px;
+$uni-spacing-col-base: 12px;
+$uni-spacing-col-lg: 16px;
+$uni-spacing-col-xl: 20px;
+
+/* 透明度 */
+$uni-opacity-disabled: 0.3;
+
+/* 文章场景相关 */
+$uni-color-title: #2c405a;
+$uni-font-size-title: 20px;
+$uni-color-subtitle: #555;
+$uni-font-size-subtitle: 18px;
+$uni-color-paragraph: #3f536e;
+$uni-font-size-paragraph: 15px;
+
+/* 动画 */
+$uni-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+$uni-transition-fast: all 0.15s ease-out;
+
+/* 主题色彩变量 - CSS变量版本 */
+:root {
+  --primary-color: rgb(93, 135, 255);
+  --primary-light: rgba(93, 135, 255, 0.1);
+  --primary-dark: rgb(73, 115, 235);
+  --success-color: #52c41a;
+  --warning-color: #faad14;
+  --error-color: #ff4d4f;
+  --text-primary: #262626;
+  --text-secondary: #595959;
+  --text-disabled: #bfbfbf;
+  --border-color: #f0f0f0;
+  --background-color: #f8f9fa;
+  --white: #ffffff;
+  --shadow-light: 0 2px 8px rgba(0, 0, 0, 0.06);
+  --shadow-medium: 0 4px 16px rgba(0, 0, 0, 0.12);
+  --radius-small: 4px;
+  --radius-medium: 8px;
+  --radius-large: 12px;
+  --spacing-xs: 4px;
+  --spacing-sm: 8px;
+  --spacing-md: 16px;
+  --spacing-lg: 24px;
+  --spacing-xl: 32px;
+  --transition-fast: 0.2s ease;
+  --transition-medium: 0.3s ease;
+  --font-size-xs: 12px;
+  --font-size-sm: 14px;
+  --font-size-md: 16px;
+  --font-size-lg: 18px;
+  --font-size-xl: 20px;
+}
+
+/* 全局样式重置 */
+* {
+  box-sizing: border-box;
+  margin: 0;
+  padding: 0;
+}
+
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+  color: var(--text-primary);
+  background-color: var(--background-color);
+  line-height: 1.6;
+}
+
+page {
+  background-color: var(--background-color);
+  color: var(--text-primary);
+  font-size: var(--font-size-md);
+  line-height: 1.6;
+}
+
+/* 通用工具类 */
+.flex {
+  display: flex;
+}
+
+.flex-center {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.flex-between {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.flex-column {
+  display: flex;
+  flex-direction: column;
+}
+
+.text-center {
+  text-align: center;
+}
+
+.text-primary {
+  color: var(--primary-color);
+}
+
+.text-secondary {
+  color: var(--text-secondary);
+}
+
+.bg-primary {
+  background-color: var(--primary-color);
+}
+
+.bg-white {
+  background-color: var(--white);
+}
+
+.rounded {
+  border-radius: var(--radius-medium);
+}
+
+.shadow {
+  box-shadow: var(--shadow-light);
+}
+
+.mt-sm { margin-top: var(--spacing-sm); }
+.mt-md { margin-top: var(--spacing-md); }
+.mt-lg { margin-top: var(--spacing-lg); }
+.mb-sm { margin-bottom: var(--spacing-sm); }
+.mb-md { margin-bottom: var(--spacing-md); }
+.mb-lg { margin-bottom: var(--spacing-lg); }
+.ml-sm { margin-left: var(--spacing-sm); }
+.ml-md { margin-left: var(--spacing-md); }
+.mr-sm { margin-right: var(--spacing-sm); }
+.mr-md { margin-right: var(--spacing-md); }
+
+.p-sm { padding: var(--spacing-sm); }
+.p-md { padding: var(--spacing-md); }
+.p-lg { padding: var(--spacing-lg); }
+.px-sm { padding-left: var(--spacing-sm); padding-right: var(--spacing-sm); }
+.px-md { padding-left: var(--spacing-md); padding-right: var(--spacing-md); }
+.py-sm { padding-top: var(--spacing-sm); padding-bottom: var(--spacing-sm); }
+.py-md { padding-top: var(--spacing-md); padding-bottom: var(--spacing-md); }
+
+/* 按钮基础样式 */
+.btn {
+  padding: var(--spacing-sm) var(--spacing-md);
+  border-radius: var(--radius-medium);
+  border: none;
+  font-size: var(--font-size-md);
+  cursor: pointer;
+  transition: var(--transition-fast);
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  text-decoration: none;
+}
+
+.btn-primary {
+  background-color: var(--primary-color);
+  color: var(--white);
+}
+
+.btn-primary:hover {
+  background-color: var(--primary-dark);
+}
+
+/* 输入框样式 */
+.input {
+  padding: var(--spacing-sm) var(--spacing-md);
+  border: 1px solid var(--border-color);
+  border-radius: var(--radius-medium);
+  font-size: var(--font-size-md);
+  transition: var(--transition-fast);
+}
+
+.input:focus {
+  border-color: var(--primary-color);
+  outline: none;
+  box-shadow: 0 0 0 2px rgba(93, 135, 255, 0.2);
+}
+
+/* 卡片样式 */
+.card {
+  background: var(--white);
+  border-radius: var(--radius-large);
+  box-shadow: var(--shadow-light);
+  padding: var(--spacing-md);
+  margin-bottom: var(--spacing-md);
+}
+
+.card-header {
+  font-size: var(--font-size-lg);
+  font-weight: 600;
+  margin-bottom: var(--spacing-md);
+  color: var(--text-primary);
+}
+
+/* 动画类 */
+.fade-in {
+  animation: fadeIn var(--transition-medium);
+}
+
+.slide-up {
+  animation: slideUp var(--transition-medium);
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+@keyframes slideUp {
+  from { 
+    opacity: 0; 
+    transform: translateY(20px); 
+  }
+  to { 
+    opacity: 1; 
+    transform: translateY(0); 
+  }
+}
+
+/* 响应式断点 */
+@media (max-width: 768px) {
+  .card {
+    margin: var(--spacing-sm);
+    padding: var(--spacing-sm);
+  }
 }

+ 158 - 0
mobile/src/utils/encrypt-browser.js

@@ -0,0 +1,158 @@
+// @ts-nocheck
+import CryptoJS from 'crypto-js';
+
+// 配置信息
+const ENCRYPT_CONFIG = {
+    // RSA公钥 - 与后端保持一致 (更新为正确版本,包含ijT7h)
+    RSA_PUBLIC_KEY:
+        'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
+    CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
+    TENANT_ID: '000000',
+};
+
+/**
+ * 生成随机AES密钥
+ * @param {number} length 密钥长度,默认32位
+ * @returns {string} AES密钥
+ */
+export function generateRandomAESKey(length = 32) {
+    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+    let result = '';
+    for (let i = 0; i < length; i++) {
+        result += characters.charAt(Math.floor(Math.random() * characters.length));
+    }
+    return result;
+}
+
+/**
+ * AES加密数据
+ * @param {string} data 待加密的数据
+ * @param {string} key AES密钥
+ * @returns {string} 加密后的数据
+ */
+export function encryptWithAES(data, key) {
+    const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
+    const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
+        mode: CryptoJS.mode.ECB,
+        padding: CryptoJS.pad.Pkcs7,
+    });
+    return encrypted.toString();
+}
+
+/**
+ * RSA加密AES密钥 - 浏览器兼容版本
+ * @param {string} aesKey AES密钥
+ * @returns {Promise<string|null>} RSA加密后的AES密钥
+ */
+export async function encryptAESKeyWithRSA(aesKey) {
+    try {
+        // 将Base64公钥转换为ArrayBuffer
+        const publicKeyData = Uint8Array.from(atob(ENCRYPT_CONFIG.RSA_PUBLIC_KEY), (c) =>
+            c.charCodeAt(0),
+        );
+
+        // 导入RSA公钥
+        const publicKey = await crypto.subtle.importKey(
+            'spki',
+            publicKeyData,
+            {
+                name: 'RSA-OAEP',
+                hash: 'SHA-1',
+            },
+            false,
+            ['encrypt'],
+        );
+
+        // 将AES密钥转为Base64
+        const aesKeyBase64 = btoa(aesKey);
+        const encoder = new TextEncoder();
+        const data = encoder.encode(aesKeyBase64);
+
+        // RSA加密
+        const encrypted = await crypto.subtle.encrypt(
+            {
+                name: 'RSA-OAEP',
+            },
+            publicKey,
+            data,
+        );
+
+        // 转换为Base64
+        const encryptedArray = new Uint8Array(encrypted);
+        return btoa(String.fromCharCode.apply(null, encryptedArray));
+    } catch (error) {
+        console.error('RSA加密失败:', error.message);
+        console.log('🔄 回退到简化加密方案...');
+
+        // 回退方案:使用AES密钥的Base64编码(临时方案)
+        try {
+            return btoa(aesKey);
+        } catch (fallbackError) {
+            console.error('回退加密也失败:', fallbackError.message);
+            return null;
+        }
+    }
+}
+
+/**
+ * 登录数据加密处理
+ * @param {Object} loginData 登录数据对象
+ * @returns {Promise<Object>} 包含加密数据和请求头的对象
+ */
+export async function encryptLoginData(loginData) {
+    try {
+        // 1. 生成AES密钥
+        const aesKey = generateRandomAESKey(32);
+        console.log('🔑 生成AES密钥:', aesKey);
+
+        // 2. 准备完整的登录数据
+        const fullLoginData = {
+            tenantId: loginData.tenantId || ENCRYPT_CONFIG.TENANT_ID,
+            username: loginData.username,
+            password: loginData.password,
+            clientId: loginData.clientId || ENCRYPT_CONFIG.CLIENT_ID,
+            grantType: loginData.grantType || 'password',
+        };
+
+        console.log('📋 登录数据:', JSON.stringify(fullLoginData, null, 2));
+
+        // 3. AES加密登录数据
+        const encryptedData = encryptWithAES(JSON.stringify(fullLoginData), aesKey);
+        console.log('🔐 AES加密后数据长度:', encryptedData.length);
+
+        // 4. RSA加密AES密钥
+        const encryptedKey = await encryptAESKeyWithRSA(aesKey);
+        if (!encryptedKey) {
+            throw new Error('RSA加密AES密钥失败');
+        }
+        console.log('🔑 RSA加密后密钥长度:', encryptedKey.length);
+
+        // 5. 返回加密结果和请求头
+        return {
+            success: true,
+            encryptedData: encryptedData,
+            headers: {
+                'Content-Type': 'application/json;charset=utf-8',
+                clientid: ENCRYPT_CONFIG.CLIENT_ID,
+                'encrypt-key': encryptedKey,
+                'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
+                isToken: 'false',
+                isEncrypt: 'true',
+                repeatSubmit: 'false',
+            },
+        };
+    } catch (error) {
+        console.error('登录数据加密失败:', error);
+        return {
+            success: false,
+            error: error.message,
+        };
+    }
+}
+
+/**
+ * 获取加密配置
+ */
+export function getEncryptConfig() {
+    return ENCRYPT_CONFIG;
+}

+ 151 - 0
mobile/src/utils/encrypt-jsencrypt.js

@@ -0,0 +1,151 @@
+// @ts-nocheck
+import CryptoJS from 'crypto-js';
+import JSEncrypt from 'jsencrypt';
+
+// 配置信息
+const ENCRYPT_CONFIG = {
+    // RSA公钥 - 与后端保持一致 (更新为正确版本,包含ijT7h)
+    RSA_PUBLIC_KEY:
+        'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
+    CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
+    TENANT_ID: '000000',
+};
+
+/**
+ * 生成随机AES密钥
+ * @param {number} length 密钥长度,默认32位
+ * @returns {string} AES密钥
+ */
+export function generateRandomAESKey(length = 32) {
+    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+    let result = '';
+    for (let i = 0; i < length; i++) {
+        result += characters.charAt(Math.floor(Math.random() * characters.length));
+    }
+    return result;
+}
+
+/**
+ * AES加密数据
+ * @param {string} data 待加密的数据
+ * @param {string} key AES密钥
+ * @returns {string} 加密后的数据
+ */
+export function encryptWithAES(data, key) {
+    const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
+    const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
+        mode: CryptoJS.mode.ECB,
+        padding: CryptoJS.pad.Pkcs7,
+    });
+    return encrypted.toString();
+}
+
+/**
+ * RSA加密AES密钥 - 使用JSEncrypt
+ * @param {string} aesKey AES密钥
+ * @returns {string|null} RSA加密后的AES密钥
+ */
+export function encryptAESKeyWithRSA(aesKey) {
+    try {
+        console.log('🔧 开始RSA加密,AES密钥:', aesKey);
+
+        // 构造完整的PEM格式RSA公钥
+        const pemKey = `-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqEfJl9KxCi6s3Fpjs234+uCvG
+SIDnR585F0jlTmWxbv0yYWe6xQDfnmhpJdwuG7nHEHfxIxLI/Yq+4fejDAMCAwEA
+AQ==
+-----END PUBLIC KEY-----`;
+
+        console.log('🔧 RSA公钥PEM格式已构造');
+
+        // 创建JSEncrypt实例
+        const encrypt = new JSEncrypt();
+
+        // 设置公钥
+        const keySetResult = encrypt.setPublicKey(pemKey);
+        console.log('🔧 公钥设置结果:', keySetResult);
+
+        // 将AES密钥转为Base64(与custom-login-script.js保持一致)
+        const aesKeyBase64 = btoa(unescape(encodeURIComponent(aesKey)));
+        console.log('🔧 AES密钥Base64:', aesKeyBase64);
+
+        // RSA加密
+        const encrypted = encrypt.encrypt(aesKeyBase64);
+
+        console.log('🔧 RSA加密结果:', encrypted);
+
+        if (!encrypted) {
+            throw new Error('JSEncrypt RSA加密返回null,可能是公钥格式错误或加密失败');
+        }
+
+        console.log('✅ RSA加密成功,结果长度:', encrypted.length);
+        return encrypted;
+    } catch (error) {
+        console.error('❌ RSA加密失败:', error.message);
+        console.error('❌ 错误堆栈:', error.stack);
+        return null;
+    }
+}
+
+/**
+ * 登录数据加密处理
+ * @param {Object} loginData 登录数据对象
+ * @returns {Object} 包含加密数据和请求头的对象
+ */
+export function encryptLoginData(loginData) {
+    try {
+        // 1. 生成AES密钥
+        const aesKey = generateRandomAESKey(32);
+        console.log('🔑 生成AES密钥:', aesKey);
+
+        // 2. 准备完整的登录数据
+        const fullLoginData = {
+            tenantId: loginData.tenantId || ENCRYPT_CONFIG.TENANT_ID,
+            username: loginData.username,
+            password: loginData.password,
+            clientId: loginData.clientId || ENCRYPT_CONFIG.CLIENT_ID,
+            grantType: loginData.grantType || 'password',
+        };
+
+        console.log('📋 登录数据:', JSON.stringify(fullLoginData, null, 2));
+
+        // 3. AES加密登录数据
+        const encryptedData = encryptWithAES(JSON.stringify(fullLoginData), aesKey);
+        console.log('🔐 AES加密后数据长度:', encryptedData.length);
+
+        // 4. RSA加密AES密钥
+        const encryptedKey = encryptAESKeyWithRSA(aesKey);
+        if (!encryptedKey) {
+            throw new Error('RSA加密AES密钥失败');
+        }
+        console.log('🔑 RSA加密后密钥长度:', encryptedKey.length);
+
+        // 5. 返回加密结果和请求头
+        return {
+            success: true,
+            encryptedData: encryptedData,
+            headers: {
+                'Content-Type': 'application/json;charset=utf-8',
+                clientid: ENCRYPT_CONFIG.CLIENT_ID,
+                'encrypt-key': encryptedKey,
+                'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
+                isToken: 'false',
+                isEncrypt: 'true',
+                repeatSubmit: 'false',
+            },
+        };
+    } catch (error) {
+        console.error('登录数据加密失败:', error);
+        return {
+            success: false,
+            error: error.message,
+        };
+    }
+}
+
+/**
+ * 获取加密配置
+ */
+export function getEncryptConfig() {
+    return ENCRYPT_CONFIG;
+}

+ 199 - 0
mobile/src/utils/encrypt-webcrypto.js

@@ -0,0 +1,199 @@
+// @ts-nocheck
+import CryptoJS from 'crypto-js';
+
+// 配置信息
+const ENCRYPT_CONFIG = {
+    // RSA公钥 - 与后端保持一致 (更新为正确版本,包含ijT7h)
+    RSA_PUBLIC_KEY:
+        'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
+    CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
+    TENANT_ID: '000000',
+};
+
+/**
+ * 生成随机AES密钥
+ * @param {number} length 密钥长度,默认32位
+ * @returns {string} AES密钥
+ */
+export function generateRandomAESKey(length = 32) {
+    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+    let result = '';
+    for (let i = 0; i < length; i++) {
+        result += characters.charAt(Math.floor(Math.random() * characters.length));
+    }
+    return result;
+}
+
+/**
+ * AES加密数据
+ * @param {string} data 待加密的数据
+ * @param {string} key AES密钥
+ * @returns {string} 加密后的数据
+ */
+export function encryptWithAES(data, key) {
+    const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
+    const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
+        mode: CryptoJS.mode.ECB,
+        padding: CryptoJS.pad.Pkcs7,
+    });
+    return encrypted.toString();
+}
+
+/**
+ * Base64字符串转ArrayBuffer
+ * @param {string} base64
+ * @returns {ArrayBuffer}
+ */
+function base64ToArrayBuffer(base64) {
+    const binaryString = atob(base64);
+    const bytes = new Uint8Array(binaryString.length);
+    for (let i = 0; i < binaryString.length; i++) {
+        bytes[i] = binaryString.charCodeAt(i);
+    }
+    return bytes.buffer;
+}
+
+/**
+ * ArrayBuffer转Base64字符串
+ * @param {ArrayBuffer} buffer
+ * @returns {string}
+ */
+function arrayBufferToBase64(buffer) {
+    const bytes = new Uint8Array(buffer);
+    let binary = '';
+    for (let i = 0; i < bytes.byteLength; i++) {
+        binary += String.fromCharCode(bytes[i]);
+    }
+    return btoa(binary);
+}
+
+/**
+ * 导入RSA公钥
+ * @param {string} publicKeyBase64 Base64格式的公钥
+ * @returns {Promise<CryptoKey>}
+ */
+async function importRSAPublicKey(publicKeyBase64) {
+    try {
+        const keyBuffer = base64ToArrayBuffer(publicKeyBase64);
+
+        const key = await crypto.subtle.importKey(
+            'spki',
+            keyBuffer,
+            {
+                name: 'RSA-OAEP',
+                hash: 'SHA-1',
+            },
+            false,
+            ['encrypt'],
+        );
+
+        return key;
+    } catch (error) {
+        console.error('导入RSA公钥失败:', error);
+        throw error;
+    }
+}
+
+/**
+ * RSA加密AES密钥 - 使用Web Crypto API
+ * @param {string} aesKey AES密钥
+ * @returns {Promise<string|null>} RSA加密后的AES密钥
+ */
+export async function encryptAESKeyWithRSA(aesKey) {
+    try {
+        // 检查Web Crypto API支持
+        if (!crypto || !crypto.subtle) {
+            throw new Error('当前环境不支持Web Crypto API');
+        }
+
+        // 导入RSA公钥
+        const publicKey = await importRSAPublicKey(ENCRYPT_CONFIG.RSA_PUBLIC_KEY);
+
+        // 将AES密钥转为Base64(与custom-login-script.js保持一致)
+        const aesKeyBase64 = btoa(unescape(encodeURIComponent(aesKey)));
+
+        // 将字符串转为ArrayBuffer
+        const dataBuffer = new TextEncoder().encode(aesKeyBase64);
+
+        // RSA加密
+        const encryptedBuffer = await crypto.subtle.encrypt(
+            {
+                name: 'RSA-OAEP',
+            },
+            publicKey,
+            dataBuffer,
+        );
+
+        // 转为Base64字符串
+        const encryptedBase64 = arrayBufferToBase64(encryptedBuffer);
+
+        console.log('✅ Web Crypto RSA加密成功,结果长度:', encryptedBase64.length);
+        return encryptedBase64;
+    } catch (error) {
+        console.error('Web Crypto RSA加密失败:', error.message);
+        return null;
+    }
+}
+
+/**
+ * 登录数据加密处理
+ * @param {Object} loginData 登录数据对象
+ * @returns {Promise<Object>} 包含加密数据和请求头的对象
+ */
+export async function encryptLoginData(loginData) {
+    try {
+        // 1. 生成AES密钥
+        const aesKey = generateRandomAESKey(32);
+        console.log('🔑 生成AES密钥:', aesKey);
+
+        // 2. 准备完整的登录数据
+        const fullLoginData = {
+            tenantId: loginData.tenantId || ENCRYPT_CONFIG.TENANT_ID,
+            username: loginData.username,
+            password: loginData.password,
+            clientId: loginData.clientId || ENCRYPT_CONFIG.CLIENT_ID,
+            grantType: loginData.grantType || 'password',
+        };
+
+        console.log('📋 登录数据:', JSON.stringify(fullLoginData, null, 2));
+
+        // 3. AES加密登录数据
+        const encryptedData = encryptWithAES(JSON.stringify(fullLoginData), aesKey);
+        console.log('🔐 AES加密后数据长度:', encryptedData.length);
+
+        // 4. RSA加密AES密钥 - 异步操作
+        const encryptedKey = await encryptAESKeyWithRSA(aesKey);
+        if (!encryptedKey) {
+            throw new Error('RSA加密AES密钥失败');
+        }
+        console.log('🔑 RSA加密后密钥长度:', encryptedKey.length);
+
+        // 5. 返回加密结果和请求头
+        return {
+            success: true,
+            encryptedData: encryptedData,
+            headers: {
+                'Content-Type': 'application/json;charset=utf-8',
+                clientid: ENCRYPT_CONFIG.CLIENT_ID,
+                'encrypt-key': encryptedKey,
+                'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
+                isToken: 'false',
+                isEncrypt: 'true',
+                repeatSubmit: 'false',
+            },
+        };
+    } catch (error) {
+        console.error('登录数据加密失败:', error);
+        return {
+            success: false,
+            error: error.message,
+        };
+    }
+}
+
+/**
+ * 获取加密配置
+ */
+export function getEncryptConfig() {
+    return ENCRYPT_CONFIG;
+}

+ 126 - 0
mobile/src/utils/encrypt.js

@@ -0,0 +1,126 @@
+// @ts-nocheck
+import CryptoJS from 'crypto-js';
+import NodeRSA from 'node-rsa';
+
+// 配置信息
+const ENCRYPT_CONFIG = {
+    // RSA公钥 - 与后端保持一致
+    RSA_PUBLIC_KEY: 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7h96MCAwEAAQ==',
+    CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
+    TENANT_ID: '000000'
+};
+
+/**
+ * 生成随机AES密钥
+ * @param {number} length 密钥长度,默认32位
+ * @returns {string} AES密钥
+ */
+export function generateRandomAESKey(length = 32) {
+    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+    let result = '';
+    for (let i = 0; i < length; i++) {
+        result += characters.charAt(Math.floor(Math.random() * characters.length));
+    }
+    return result;
+}
+
+/**
+ * AES加密数据
+ * @param {string} data 待加密的数据
+ * @param {string} key AES密钥
+ * @returns {string} 加密后的数据
+ */
+export function encryptWithAES(data, key) {
+    const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
+    const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
+        mode: CryptoJS.mode.ECB,
+        padding: CryptoJS.pad.Pkcs7,
+    });
+    return encrypted.toString();
+}
+
+/**
+ * RSA加密AES密钥
+ * @param {string} aesKey AES密钥
+ * @returns {string|null} RSA加密后的AES密钥
+ */
+export function encryptAESKeyWithRSA(aesKey) {
+    try {
+        const pemKey = `-----BEGIN PUBLIC KEY-----\n${ENCRYPT_CONFIG.RSA_PUBLIC_KEY}\n-----END PUBLIC KEY-----`;
+        const rsaKey = new NodeRSA(pemKey);
+        rsaKey.setOptions({ encryptionScheme: 'pkcs1' });
+
+        // 将AES密钥转为Base64
+        const aesKeyBase64 = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(aesKey));
+
+        // RSA加密
+        return rsaKey.encrypt(aesKeyBase64, 'base64');
+    } catch (error) {
+        console.error('RSA加密失败:', error.message);
+        return null;
+    }
+}
+
+/**
+ * 登录数据加密处理
+ * @param {Object} loginData 登录数据对象
+ * @returns {Object} 包含加密数据和请求头的对象
+ */
+export function encryptLoginData(loginData) {
+    try {
+        // 1. 生成AES密钥
+        const aesKey = generateRandomAESKey(32);
+        console.log('🔑 生成AES密钥:', aesKey);
+
+        // 2. 准备完整的登录数据
+        const fullLoginData = {
+            tenantId: loginData.tenantId || ENCRYPT_CONFIG.TENANT_ID,
+            username: loginData.username,
+            password: loginData.password,
+            clientId: loginData.clientId || ENCRYPT_CONFIG.CLIENT_ID,
+            grantType: loginData.grantType || 'password',
+        };
+
+        console.log('📋 登录数据:', JSON.stringify(fullLoginData, null, 2));
+
+        // 3. AES加密登录数据
+        const encryptedData = encryptWithAES(JSON.stringify(fullLoginData), aesKey);
+        console.log('🔐 AES加密后数据长度:', encryptedData.length);
+
+        // 4. RSA加密AES密钥
+        const encryptedKey = encryptAESKeyWithRSA(aesKey);
+        if (!encryptedKey) {
+            throw new Error('RSA加密AES密钥失败');
+        }
+        console.log('🔑 RSA加密后密钥长度:', encryptedKey.length);
+
+        // 5. 返回加密结果和请求头
+        return {
+            success: true,
+            encryptedData: encryptedData,
+            headers: {
+                'Content-Type': 'application/json;charset=utf-8',
+                clientid: ENCRYPT_CONFIG.CLIENT_ID,
+                'encrypt-key': encryptedKey,
+                'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
+                isToken: 'false',
+                isEncrypt: 'true',
+                repeatSubmit: 'false',
+            }
+        };
+
+    } catch (error) {
+        console.error('登录数据加密失败:', error);
+        return {
+            success: false,
+            error: error.message
+        };
+    }
+}
+
+/**
+ * 获取加密配置
+ */
+export function getEncryptConfig() {
+    return ENCRYPT_CONFIG;
+} 

+ 57 - 2
mobile/src/utils/request.js

@@ -30,8 +30,13 @@ export const request = (options) => {
                     if (res.data && res.data.code !== undefined) {
                         if (res.data.code === 200) {
                             resolve(res.data);
+                        } else if (res.data.code === 401) {
+                            // 401未授权,跳转到登录页
+                            console.log('🔒 Token失效,自动跳转到登录页');
+                            handleUnauthorized(res.data.msg || '登录已失效,请重新登录');
+                            reject(res.data);
                         } else {
-                            // 业务错误
+                            // 其他业务错误
                             uni.showToast({
                                 title: res.data.msg || '请求失败',
                                 icon: 'none',
@@ -41,8 +46,13 @@ export const request = (options) => {
                     } else {
                         resolve(res.data);
                     }
+                } else if (res.statusCode === 401) {
+                    // HTTP 401未授权
+                    console.log('🔒 HTTP 401未授权,自动跳转到登录页');
+                    handleUnauthorized('登录已失效,请重新登录');
+                    reject(res);
                 } else {
-                    // HTTP状态码错误
+                    // 其他HTTP状态码错误
                     uni.showToast({
                         title: `请求失败: ${res.statusCode}`,
                         icon: 'none',
@@ -76,6 +86,51 @@ function getToken() {
     }
 }
 
+// 处理401未授权响应
+function handleUnauthorized(message = '登录已失效,请重新登录') {
+    console.log('🔒 处理401未授权响应:', message);
+
+    // 清除登录信息
+    try {
+        uni.removeStorageSync('access_token');
+        uni.removeStorageSync('userInfo');
+        uni.removeStorageSync('isLoggedIn');
+        console.log('📤 已清除本地登录信息');
+    } catch (e) {
+        console.error('❌ 清除登录信息失败:', e);
+    }
+
+    // 显示提示信息
+    uni.showToast({
+        title: message,
+        icon: 'none',
+        duration: 2000,
+    });
+
+    // 延迟跳转,确保Toast显示
+    setTimeout(() => {
+        // 获取当前页面路径
+        const pages = getCurrentPages();
+        const currentPage = pages[pages.length - 1];
+        const currentPath = '/' + currentPage.route;
+
+        // 如果当前不是登录页,则跳转到登录页
+        if (currentPath !== '/pages/login/index') {
+            console.log('🔄 从', currentPath, '跳转到登录页');
+            uni.reLaunch({
+                url: '/pages/login/index',
+                fail: (error) => {
+                    console.error('❌ 跳转登录页失败:', error);
+                    // 如果reLaunch失败,尝试其他方式
+                    uni.redirectTo({
+                        url: '/pages/login/index',
+                    });
+                },
+            });
+        }
+    }, 500);
+}
+
 // API方法封装
 export const api = {
     // 用户登录

+ 259 - 0
mobile/src/utils/route-guard.js

@@ -0,0 +1,259 @@
+/**
+ * 全局路由守卫
+ * 处理页面访问权限控制和登录状态检查
+ */
+
+import { userStorage } from './request';
+import { userState } from './user';
+
+// 无需登录验证的页面列表
+const PUBLIC_PAGES = [
+    '/pages/login/index',
+    '/pages/index/index', // 首页作为公开页面,可以访问但功能受限
+];
+
+// 登录页路径
+const LOGIN_PAGE = '/pages/login/index';
+
+/**
+ * 检查页面是否为公开页面
+ * @param {string} path 页面路径
+ */
+function isPublicPage(path) {
+    return PUBLIC_PAGES.some((publicPath) => {
+        // 精确匹配或前缀匹配
+        return path === publicPath || path.startsWith(publicPath + '?');
+    });
+}
+
+/**
+ * 获取当前页面路径
+ */
+function getCurrentPagePath() {
+    try {
+        const pages = getCurrentPages();
+        if (pages.length > 0) {
+            const currentPage = pages[pages.length - 1];
+            return '/' + currentPage.route;
+        }
+    } catch (e) {
+        console.error('获取当前页面路径失败:', e);
+    }
+    return '';
+}
+
+/**
+ * 执行登录检查和跳转
+ * @param {string} redirectReason 跳转原因
+ */
+function redirectToLogin(redirectReason = '请先登录') {
+    console.log('🔄 重定向到登录页:', redirectReason);
+
+    // 清除可能存在的无效登录信息
+    userStorage.clearLoginInfo();
+
+    // 提示用户
+    uni.showToast({
+        title: redirectReason,
+        icon: 'none',
+        duration: 2000,
+    });
+
+    // 延迟跳转,确保Toast显示
+    setTimeout(() => {
+        uni.reLaunch({
+            url: LOGIN_PAGE,
+            fail: (error) => {
+                console.error('跳转登录页失败:', error);
+                // 如果reLaunch失败,尝试其他方式
+                uni.redirectTo({
+                    url: LOGIN_PAGE,
+                });
+            },
+        });
+    }, 500);
+}
+
+/**
+ * 全局页面守卫检查
+ * 在每个页面的onLoad或onShow中调用
+ * @param {string} pagePath 页面路径(可选,自动获取当前页面)
+ */
+export function checkPageAccess(pagePath = null) {
+    try {
+        // 获取页面路径
+        const currentPath = pagePath || getCurrentPagePath();
+        console.log('🔍 检查页面访问权限:', currentPath);
+
+        // 如果是公开页面,允许访问
+        if (isPublicPage(currentPath)) {
+            console.log('✅ 公开页面,允许访问');
+            return true;
+        }
+
+        // 检查登录状态
+        if (!userStorage.isLoggedIn()) {
+            console.log('❌ 用户未登录');
+            redirectToLogin('请先登录');
+            return false;
+        }
+
+        // 检查Token是否存在
+        const token = userStorage.getToken();
+        if (!token) {
+            console.log('❌ Token不存在');
+            redirectToLogin('登录信息无效,请重新登录');
+            return false;
+        }
+
+        // 检查Token是否过期
+        if (userState.checkTokenExpiration()) {
+            console.log('❌ Token已过期');
+            redirectToLogin('登录已过期,请重新登录');
+            return false;
+        }
+
+        console.log('✅ 页面访问权限验证通过');
+        return true;
+    } catch (error) {
+        console.error('页面访问权限检查失败:', error);
+        redirectToLogin('系统错误,请重新登录');
+        return false;
+    }
+}
+
+/**
+ * 页面导航守卫
+ * 在页面跳转前进行权限检查
+ * @param {string} url 目标页面URL
+ * @param {object} options 跳转选项
+ */
+export function guardedNavigate(url, options = {}) {
+    try {
+        console.log('🚀 准备跳转到:', url);
+
+        // 解析URL获取页面路径
+        const urlPath = url.split('?')[0];
+
+        // 如果目标是公开页面,直接跳转
+        if (isPublicPage(urlPath)) {
+            return uni.navigateTo({ url, ...options });
+        }
+
+        // 检查登录状态
+        if (!userStorage.isLoggedIn()) {
+            redirectToLogin('请先登录后访问');
+            return false;
+        }
+
+        // 检查Token有效性
+        const token = userStorage.getToken();
+        if (!token || userState.checkTokenExpiration()) {
+            redirectToLogin('登录已失效,请重新登录');
+            return false;
+        }
+
+        // 执行跳转
+        return uni.navigateTo({ url, ...options });
+    } catch (error) {
+        console.error('页面跳转守卫失败:', error);
+        uni.showToast({
+            title: '页面跳转失败',
+            icon: 'none',
+        });
+        return false;
+    }
+}
+
+/**
+ * Tab页面切换守卫
+ * @param {string} url Tab页面URL
+ */
+export function guardedSwitchTab(url) {
+    try {
+        console.log('🔄 准备切换Tab到:', url);
+
+        const urlPath = url.split('?')[0];
+
+        // 如果是公开Tab页面,允许切换
+        if (isPublicPage(urlPath)) {
+            return uni.switchTab({ url });
+        }
+
+        // 检查登录状态
+        if (!userStorage.isLoggedIn() || !userStorage.getToken()) {
+            redirectToLogin('请先登录');
+            return false;
+        }
+
+        // 检查Token有效性
+        if (userState.checkTokenExpiration()) {
+            redirectToLogin('登录已过期,请重新登录');
+            return false;
+        }
+
+        return uni.switchTab({ url });
+    } catch (error) {
+        console.error('Tab切换守卫失败:', error);
+        return false;
+    }
+}
+
+/**
+ * 全局混入方法,用于在页面中自动检查权限
+ */
+export const routeGuardMixin = {
+    onLoad() {
+        // 页面加载时检查权限
+        this.checkPageAccess();
+    },
+
+    onShow() {
+        // 页面显示时检查权限(防止后台切换回来时状态失效)
+        this.checkPageAccess();
+    },
+
+    methods: {
+        // 在页面中可直接调用的权限检查方法
+        checkPageAccess() {
+            return checkPageAccess();
+        },
+
+        // 带权限检查的页面跳转
+        guardedNavigate(url, options) {
+            return guardedNavigate(url, options);
+        },
+
+        // 带权限检查的Tab切换
+        guardedSwitchTab(url) {
+            return guardedSwitchTab(url);
+        },
+    },
+};
+
+/**
+ * 初始化路由守卫
+ * 在App.vue中调用
+ */
+export function initRouteGuard() {
+    console.log('🛡️ 初始化全局路由守卫');
+
+    // 页面加载完成后检查当前页面权限
+    uni.$on('pageLoad', () => {
+        checkPageAccess();
+    });
+
+    // 监听应用进入前台
+    uni.$on('onShow', () => {
+        console.log('📱 应用进入前台,检查登录状态');
+        checkPageAccess();
+    });
+}
+
+export default {
+    checkPageAccess,
+    guardedNavigate,
+    guardedSwitchTab,
+    routeGuardMixin,
+    initRouteGuard,
+};

+ 315 - 0
mobile/src/utils/user.js

@@ -0,0 +1,315 @@
+/**
+ * 用户管理工具
+ * 处理用户信息、权限、状态等相关功能
+ */
+
+import { userStorage } from './request';
+
+/**
+ * 用户信息管理
+ */
+export const userManager = {
+    /**
+     * 获取当前用户信息
+     */
+    getCurrentUser() {
+        const userInfo = userStorage.getUserInfo();
+        if (!userInfo || !userInfo.user) {
+            return null;
+        }
+
+        const user = userInfo.user;
+        const permissions = userInfo.permissions || [];
+        const roles = userInfo.roles || [];
+
+        return {
+            // 基本信息
+            userId: user.userId,
+            userName: user.userName,
+            nickName: user.nickName || user.userName,
+            email: user.email,
+            phoneNumber: user.phoneNumber,
+            sex: user.sex,
+            avatar: user.avatar,
+
+            // 部门信息
+            deptId: user.deptId,
+            deptName: user.dept?.deptName || '',
+
+            // 状态信息
+            status: user.status,
+            createTime: user.createTime,
+            loginDate: user.loginDate,
+
+            // 权限信息
+            permissions,
+            roles,
+
+            // 扩展信息
+            remark: user.remark,
+        };
+    },
+
+    /**
+     * 检查用户是否拥有指定权限
+     * @param {string} permission 权限标识
+     */
+    hasPermission(permission) {
+        if (!permission) return true;
+
+        const userInfo = this.getCurrentUser();
+        if (!userInfo || !userInfo.permissions) {
+            return false;
+        }
+
+        return userInfo.permissions.includes(permission);
+    },
+
+    /**
+     * 检查用户是否拥有指定角色
+     * @param {string} role 角色标识
+     */
+    hasRole(role) {
+        if (!role) return true;
+
+        const userInfo = this.getCurrentUser();
+        if (!userInfo || !userInfo.roles) {
+            return false;
+        }
+
+        return userInfo.roles.some((r) => r.roleKey === role);
+    },
+
+    /**
+     * 检查用户是否为管理员
+     */
+    isAdmin() {
+        return this.hasRole('admin') || this.hasRole('super_admin');
+    },
+
+    /**
+     * 获取用户显示名称
+     */
+    getDisplayName() {
+        const user = this.getCurrentUser();
+        if (!user) return '未登录';
+
+        return user.nickName || user.userName || '用户';
+    },
+
+    /**
+     * 获取用户头像URL
+     * @param {string} defaultAvatar 默认头像URL
+     */
+    getAvatarUrl(defaultAvatar = '/static/default-avatar.png') {
+        const user = this.getCurrentUser();
+        if (!user || !user.avatar) {
+            return defaultAvatar;
+        }
+
+        // 如果是完整URL,直接返回
+        if (user.avatar.startsWith('http')) {
+            return user.avatar;
+        }
+
+        // 拼接基础URL
+        const baseURL =
+            process.env.NODE_ENV === 'development'
+                ? 'http://localhost:8080'
+                : 'https://your-api-domain.com';
+
+        return baseURL + user.avatar;
+    },
+
+    /**
+     * 更新用户本地信息
+     * @param {object} userData 用户数据
+     */
+    updateUserInfo(userData) {
+        try {
+            const currentInfo = userStorage.getUserInfo();
+            if (currentInfo) {
+                const updatedInfo = {
+                    ...currentInfo,
+                    user: {
+                        ...currentInfo.user,
+                        ...userData,
+                    },
+                };
+                uni.setStorageSync('userInfo', JSON.stringify(updatedInfo));
+                console.log('用户信息已更新');
+                return true;
+            }
+        } catch (e) {
+            console.error('更新用户信息失败:', e);
+        }
+        return false;
+    },
+
+    /**
+     * 格式化用户信息用于显示
+     */
+    formatUserForDisplay() {
+        const user = this.getCurrentUser();
+        if (!user) return null;
+
+        return {
+            name: this.getDisplayName(),
+            avatar: this.getAvatarUrl(),
+            department: user.deptName || '未分配部门',
+            phone: user.phoneNumber || '未设置',
+            email: user.email || '未设置',
+            status: user.status === '0' ? '正常' : '停用',
+            isAdmin: this.isAdmin(),
+            lastLoginTime: user.loginDate ? new Date(user.loginDate).toLocaleString() : '首次登录',
+        };
+    },
+};
+
+/**
+ * 权限检查指令工具
+ */
+export const permissionUtils = {
+    /**
+     * 检查页面访问权限
+     * @param {string} pagePath 页面路径
+     */
+    checkPagePermission(pagePath) {
+        // 公开页面,无需权限检查
+        const publicPages = ['/pages/login/index', '/pages/index/index', '/pages/profile/index'];
+
+        if (publicPages.includes(pagePath)) {
+            return true;
+        }
+
+        // 检查是否已登录
+        if (!userStorage.isLoggedIn()) {
+            return false;
+        }
+
+        // 这里可以根据具体业务需求添加页面权限检查逻辑
+        return true;
+    },
+
+    /**
+     * 权限控制的页面跳转
+     * @param {string} url 目标页面URL
+     * @param {string} permission 所需权限
+     */
+    navigateWithPermission(url, permission = null) {
+        // 检查登录状态
+        if (!userStorage.isLoggedIn()) {
+            uni.showToast({
+                title: '请先登录',
+                icon: 'none',
+            });
+            uni.reLaunch({
+                url: '/pages/login/index',
+            });
+            return false;
+        }
+
+        // 检查权限
+        if (permission && !userManager.hasPermission(permission)) {
+            uni.showToast({
+                title: '权限不足',
+                icon: 'none',
+            });
+            return false;
+        }
+
+        // 执行跳转
+        uni.navigateTo({
+            url,
+            fail: () => {
+                uni.showToast({
+                    title: '页面跳转失败',
+                    icon: 'none',
+                });
+            },
+        });
+
+        return true;
+    },
+};
+
+/**
+ * 用户状态管理
+ */
+export const userState = {
+    /**
+     * 检查Token是否即将过期(提前5分钟提醒)
+     */
+    checkTokenExpiration() {
+        try {
+            const token = userStorage.getToken();
+            if (!token) return false;
+
+            // 这里可以解析JWT token获取过期时间
+            // 简化处理:检查登录时间是否超过2小时
+            const userInfo = userStorage.getUserInfo();
+            if (userInfo && userInfo.loginTime) {
+                const loginTime = new Date(userInfo.loginTime).getTime();
+                const now = Date.now();
+                const twoHours = 2 * 60 * 60 * 1000; // 2小时
+
+                return now - loginTime > twoHours;
+            }
+        } catch (e) {
+            console.error('检查Token过期时间失败:', e);
+        }
+
+        return false;
+    },
+
+    /**
+     * 刷新Token
+     */
+    async refreshToken() {
+        try {
+            // 这里调用刷新Token的API
+            // const result = await api.refreshToken();
+            // if (result.code === 200) {
+            //     uni.setStorageSync('access_token', result.data.access_token);
+            //     return true;
+            // }
+            console.log('Token刷新功能待实现');
+        } catch (e) {
+            console.error('刷新Token失败:', e);
+        }
+        return false;
+    },
+
+    /**
+     * 自动登录检查
+     */
+    autoLoginCheck() {
+        if (!userStorage.isLoggedIn()) {
+            return false;
+        }
+
+        // 检查Token是否过期
+        if (this.checkTokenExpiration()) {
+            uni.showModal({
+                title: '登录过期',
+                content: '您的登录已过期,请重新登录',
+                showCancel: false,
+                success: () => {
+                    userStorage.clearLoginInfo();
+                    uni.reLaunch({
+                        url: '/pages/login/index',
+                    });
+                },
+            });
+            return false;
+        }
+
+        return true;
+    },
+};
+
+export default {
+    userManager,
+    permissionUtils,
+    userState,
+};