Files
hzdyjs/.cursor/cursorrules-mobile.md
T
zlwu afe3e234e9 feat(mobile): 实现全局路由守卫功能,自动处理用户未登录跳转
✨ 新增功能:
- 全局路由守卫模块 (route-guard.js)
- 多层次权限验证 (登录状态+Token存在+Token过期检查)
- 自动拦截机制 (页面级+跳转级+HTTP级+应用级)
- 401响应自动处理和跳转
- 路由守卫测试页面

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

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

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

🎯 核心效果:
用户登录失效或未登录时,访问任何需要权限的页面都自动跳转到登录页,提升安全性和用户体验。
2025-07-04 22:28:49 +08:00

45 KiB
Raw Blame History

Cursor Rules - 移动端开发规范 (Vue 3 + uniapp + uview-plus)

适用范围

  • 项目目录: mobile/ (现代化智慧校园移动应用)
  • 开发语言: JavaScript (Vue 3)
  • 平台支持: H5、小程序、APP、鸿蒙
  • UI 框架: uView UI 2.x (uview-plus 3.4.45)

智慧校园移动端开发规范

1. 🎯 技术栈规范

核心技术

  • 基础框架: uni-app + Vue 3.4.21
  • UI 组件库: uview-plus 3.4.45 (Vue 3 版本)
  • 开发语言: JavaScript (ES6+)
  • 国际化: vue-i18n 9.1.9 (Vue 3 版本)
  • 样式预处理: Sass 1.89.2
  • 构建工具: Vite 5.2.8
  • 代码规范: ESLint 9.30.1

版本信息

{
    "name": "uni-preset-vue",
    "version": "0.0.0",
    "dependencies": {
        "@dcloudio/uni-app": "3.0.0-4060620250520001",
        "uview-plus": "^3.4.45",
        "vue": "^3.4.21",
        "vue-i18n": "^9.1.9"
    },
    "devDependencies": {
        "sass": "^1.89.2",
        "vite": "5.2.8",
        "eslint": "^9.30.1"
    }
}

2. 📁 项目结构规范

目录结构

mobile/
├── src/                          # 源码目录
│   ├── components/               # 通用组件
│   │   ├── NavBar.vue           # 导航栏组件
│   │   ├── Card.vue             # 卡片组件
│   │   ├── ServiceItem.vue      # 服务项组件
│   │   ├── StatCard.vue         # 统计卡片组件
│   │   └── GridLayout.vue       # 网格布局组件
│   ├── pages/                   # 页面文件
│   │   ├── index/               # 首页
│   │   │   └── index.vue        # 首页内容
│   │   ├── workbench/           # 办公中心
│   │   │   └── index.vue        # 办公中心内容
│   │   └── profile/             # 我的页面
│   │       └── index.vue        # 个人中心内容
│   ├── utils/                   # 工具类
│   │   ├── request.js           # 网络请求工具
│   │   ├── sse.js              # SSE实时数据工具
│   │   ├── device.js           # 设备检测工具
│   │   ├── animations.js       # 动画工具
│   │   ├── gestures.js         # 手势识别工具
│   │   └── accessibility.js    # 无障碍访问工具
│   ├── static/                 # 静态资源
│   │   ├── tabbar/             # 底部导航图标
│   │   └── fonts/              # 字体文件
│   ├── App.vue                 # 应用入口
│   ├── main.js                 # 项目入口
│   ├── pages.json              # 页面配置
│   ├── manifest.json           # 应用配置
│   └── uni.scss               # 全局样式
├── package.json                # 项目配置
├── vite.config.js             # Vite构建配置
├── eslint.config.cjs          # ESLint配置
└── README.md                  # 项目说明

3. 🎨 页面开发规范

页面模板标准

<template>
    <view class="page-container">
        <!-- 自定义导航栏 -->
        <NavBar
            :title="pageTitle"
            :show-refresh="true"
            :show-search="true"
            @refresh="handleRefresh"
            @search="handleSearch"
        />

        <!-- 主要内容区域 -->
        <scroll-view class="main-content" scroll-y enhanced :show-scrollbar="false">
            <!-- 页面内容 -->
            <view class="content">
                <!-- 使用uView-plus组件 -->
                <u-cell-group>
                    <u-cell title="示例项目"></u-cell>
                </u-cell-group>
            </view>

            <!-- 底部间距 -->
            <view class="bottom-spacer"></view>
        </scroll-view>
    </view>
</template>

<script>
import NavBar from '@/components/NavBar.vue';

export default {
    name: 'PageName',
    components: {
        NavBar,
    },
    data() {
        return {
            pageTitle: '页面标题',
            // 页面数据
        };
    },
    onLoad() {
        // 页面加载逻辑
        this.initPage();
    },
    onShow() {
        // 页面显示逻辑
    },
    onUnload() {
        // 页面卸载时清理定时器等
        if (this.timer) {
            clearInterval(this.timer);
        }
    },
    methods: {
        initPage() {
            // 初始化页面逻辑
        },
        handleRefresh() {
            // 刷新逻辑
            uni.showLoading({ title: '刷新中...' });
            setTimeout(() => {
                uni.hideLoading();
                uni.showToast({ title: '刷新成功', icon: 'success' });
            }, 1000);
        },
        handleSearch() {
            // 搜索逻辑
        },
    },
};
</script>

<style lang="scss" scoped>
.page-container {
    min-height: 100vh;
    background-color: var(--background-color);
    display: flex;
    flex-direction: column;
}

.main-content {
    flex: 1;
    padding-bottom: calc(50px + env(safe-area-inset-bottom));
}

.content {
    padding: var(--spacing-md);
}

.bottom-spacer {
    height: 20px;
}
</style>

页面样式规范

使用 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;
}

工具类规范

/* 布局工具类 */
.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);
}

/* 间距工具类 */
.mt-sm {
    margin-top: var(--spacing-sm);
}
.mt-md {
    margin-top: var(--spacing-md);
}
.mt-lg {
    margin-top: var(--spacing-lg);
}
.p-sm {
    padding: var(--spacing-sm);
}
.p-md {
    padding: var(--spacing-md);
}
.p-lg {
    padding: var(--spacing-lg);
}

4. 🧩 组件开发规范

自定义组件模板

<template>
    <view class="component-wrapper" :class="computedClass">
        <!-- 组件内容 -->
        <slot></slot>
    </view>
</template>

<script>
/**
 * 智慧校园通用组件
 * @property {String} customClass 自定义样式类
 * @property {Boolean} disabled 是否禁用
 * @example <custom-component :disabled="false">内容</custom-component>
 */
export default {
    name: 'CustomComponent',
    props: {
        customClass: {
            type: String,
            default: '',
        },
        disabled: {
            type: Boolean,
            default: false,
        },
    },
    computed: {
        computedClass() {
            return {
                disabled: this.disabled,
                [this.customClass]: !!this.customClass,
            };
        },
    },
    methods: {
        handleClick() {
            if (this.disabled) return;

            this.$emit('click');
            // 触觉反馈
            uni.vibrateShort();
        },
    },
};
</script>

<style lang="scss" scoped>
.component-wrapper {
    // 组件样式
    transition: var(--transition-fast);

    &.disabled {
        opacity: 0.5;
        pointer-events: none;
    }
}
</style>

组件命名规范

  • 文件命名: 使用 PascalCase (如: NavBar.vue, ServiceItem.vue)
  • 组件名称: 使用 PascalCase (如: NavBar, ServiceItem)
  • 引用方式: 支持驼峰和短横线 (如: <NavBar> 或 <nav-bar>)

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

.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

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 - 邮箱图标

标准使用方法

基础用法

<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>

自定义样式

<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 值

/* 主题色 #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 中添加新的图标类
/* 新图标示例 */
.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)

// API请求配置
const baseURL =
    process.env.NODE_ENV === 'development'
        ? 'http://localhost:3000/api'
        : 'https://your-api-domain.com/api';

// 统一请求方法
export const request = (options) => {
    return new Promise((resolve, reject) => {
        uni.request({
            url: baseURL + options.url,
            method: options.method || 'GET',
            data: options.data || {},
            header: {
                'Content-Type': 'application/json',
                Authorization: getToken(),
                ...options.header,
            },
            success: (res) => {
                if (res.statusCode === 200) {
                    resolve(res.data);
                } else {
                    reject(res);
                }
            },
            fail: (error) => {
                reject(error);
            },
        });
    });
};

// 获取token
function getToken() {
    try {
        const token = uni.getStorageSync('token');
        return token ? `Bearer ${token}` : '';
    } catch (e) {
        return '';
    }
}

// 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',
        }),

    // 获取租户列表
    getTenantList: (isToken = false) =>
        request({
            url: '/auth/tenant/list',
            method: 'GET',
            header: {
                isToken: isToken,
            },
        }),

    // 注销登录
    logout: () =>
        request({
            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);
        }
    },
};

数据缓存规范

// 数据缓存工具 (utils/request.js)
export const storage = {
    // 设置缓存
    set: (key, value, expire) => {
        try {
            const data = {
                value,
                timestamp: Date.now(),
                expire: expire ? Date.now() + expire * 1000 : null,
            };
            uni.setStorageSync(key, JSON.stringify(data));
        } catch (e) {
            console.error('缓存设置失败:', e);
        }
    },

    // 获取缓存
    get: (key) => {
        try {
            const data = uni.getStorageSync(key);
            if (!data) return null;

            const parsed = JSON.parse(data);

            // 检查是否过期
            if (parsed.expire && Date.now() > parsed.expire) {
                uni.removeStorageSync(key);
                return null;
            }

            return parsed.value;
        } catch (e) {
            console.error('缓存获取失败:', e);
            return null;
        }
    },

    // 删除缓存
    remove: (key) => {
        try {
            uni.removeStorageSync(key);
        } catch (e) {
            console.error('缓存删除失败:', e);
        }
    },

    // 清空缓存
    clear: () => {
        try {
            uni.clearStorageSync();
        } catch (e) {
            console.error('缓存清空失败:', e);
        }
    },
};

7. 🔐 登录状态管理规范

登录页面开发规范

登录页面模板 (pages/login/index.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 生命周期中检查登录状态:

// 页面登录状态检查模板
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 || '用户';
                // 根据需要设置其他用户信息
            }
        },
    },
};

退出登录处理

// 退出登录标准流程
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 登录页配置

{
    "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. 安全性原则

// 敏感信息处理
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. 自动登录检查

// 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 过期处理

// 请求拦截器中的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 配置

{
    "pages": [
        {
            "path": "pages/index/index",
            "style": {
                "navigationBarTitleText": "首页",
                "navigationStyle": "custom"
            }
        },
        {
            "path": "pages/workbench/index",
            "style": {
                "navigationBarTitleText": "办公中心",
                "navigationStyle": "custom"
            }
        },
        {
            "path": "pages/profile/index",
            "style": {
                "navigationBarTitleText": "我的",
                "navigationStyle": "custom"
            }
        }
    ],
    "globalStyle": {
        "navigationBarTextStyle": "white",
        "navigationBarTitleText": "智慧校园",
        "navigationBarBackgroundColor": "rgb(93, 135, 255)",
        "backgroundColor": "#f8f9fa"
    },
    "tabBar": {
        "color": "#999999",
        "selectedColor": "rgb(93, 135, 255)",
        "borderStyle": "white",
        "backgroundColor": "#ffffff",
        "height": "60px",
        "fontSize": "12px",
        "iconWidth": "22px",
        "iconHeight": "22px",
        "list": [
            {
                "pagePath": "pages/index/index",
                "text": "首页",
                "iconPath": "static/tabbar/home.svg",
                "selectedIconPath": "static/tabbar/home-active.svg"
            },
            {
                "pagePath": "pages/workbench/index",
                "text": "办公中心",
                "iconPath": "static/tabbar/workbench.svg",
                "selectedIconPath": "static/tabbar/workbench-active.svg"
            },
            {
                "pagePath": "pages/profile/index",
                "text": "我的",
                "iconPath": "static/tabbar/profile.svg",
                "selectedIconPath": "static/tabbar/profile-active.svg"
            }
        ]
    }
}

manifest.json 配置

{
    "name": "智慧校园",
    "appid": "",
    "description": "基于Vue3+uniapp的智慧校园移动应用",
    "versionName": "1.0.0",
    "versionCode": "100",
    "transformPx": false,
    "vueVersion": "3",
    "app-plus": {
        "usingComponents": true,
        "nvueStyleCompiler": "uni-app",
        "compilerVersion": 3,
        "splashscreen": {
            "alwaysShowBeforeRender": true,
            "waiting": true,
            "autoclose": true,
            "delay": 0
        },
        "modules": {},
        "distribute": {
            "android": {
                "permissions": [
                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>"
                ]
            },
            "ios": {},
            "sdkConfigs": {}
        }
    },
    "quickapp": {},
    "mp-weixin": {
        "appid": "",
        "setting": {
            "urlCheck": false
        },
        "usingComponents": true
    },
    "mp-alipay": {
        "usingComponents": true
    },
    "mp-baidu": {
        "usingComponents": true
    },
    "mp-toutiao": {
        "usingComponents": true
    },
    "uniStatistics": {
        "enable": false
    }
}

9. 🎭 用户体验规范

交互设计规范

动画效果使用

// utils/animations.js - 动画工具
export const animationUtils = {
  // 淡入动画
  fadeIn: (element) => {
    element.style.animation = 'fadeIn 0.3s ease';
  },

  // 滑入动画
  slideUp: (element) => {
    element.style.animation = 'slideUp 0.3s ease';
  },

  // 缩放动画
  scale: (element) => {
    element.style.animation = 'scale 0.2s ease';
  },
}

// 在页面中使用
methods: {
  handleItemClick() {
    // 触觉反馈
    uni.vibrateShort();

    // 视觉反馈
    // 处理点击逻辑
  }
}

手势操作支持

// utils/gestures.js - 手势识别
export const gestureManager = {
    // 初始化手势
    init: (element, callbacks) => {
        let startX, startY, startTime;

        element.addEventListener('touchstart', (e) => {
            startX = e.touches[0].clientX;
            startY = e.touches[0].clientY;
            startTime = Date.now();
        });

        element.addEventListener('touchend', (e) => {
            const endX = e.changedTouches[0].clientX;
            const endY = e.changedTouches[0].clientY;
            const endTime = Date.now();

            // 判断手势类型
            const deltaX = endX - startX;
            const deltaY = endY - startY;
            const deltaTime = endTime - startTime;

            if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 50) {
                if (deltaX > 0) {
                    callbacks.swipeRight && callbacks.swipeRight();
                } else {
                    callbacks.swipeLeft && callbacks.swipeLeft();
                }
            }
        });
    },
};

无障碍访问规范

// utils/accessibility.js - 无障碍访问工具
export const accessibilityUtils = {
    // 设置焦点
    setFocus: (element) => {
        element.focus();
    },

    // 添加ARIA标签
    addAriaLabel: (element, label) => {
        element.setAttribute('aria-label', label);
    },

    // 键盘导航支持
    enableKeyboardNavigation: (container) => {
        container.addEventListener('keydown', (e) => {
            switch (e.key) {
                case 'Tab':
                    // Tab导航逻辑
                    break;
                case 'Enter':
                case ' ':
                    // 激活元素
                    e.target.click();
                    break;
                case 'Escape':
                    // 取消操作
                    break;
            }
        });
    },
};

10. 🚀 性能优化规范

代码优化

组件懒加载

// 页面中使用懒加载
export default {
    components: {
        // 懒加载组件
        LazyComponent: () => import('@/components/LazyComponent.vue'),
    },
};

图片优化

<template>
    <!-- 响应式图片 -->
    <image :src="imageSrc" mode="aspectFit" lazy-load :fade-show="true" @error="handleImageError" />
</template>

<script>
export default {
    methods: {
        handleImageError() {
            // 图片加载失败处理
            console.log('图片加载失败');
        },
    },
};
</script>

网络优化

请求缓存策略

// 智能缓存策略
export const cacheStrategy = {
    // 短期缓存 (5分钟)
    short: 5 * 60,
    // 中期缓存 (30分钟)
    medium: 30 * 60,
    // 长期缓存 (24小时)
    long: 24 * 60 * 60,
};

// API调用示例
export const api = {
    getUserInfo: () => {
        const cacheKey = 'userInfo';
        const cached = storage.get(cacheKey);

        if (cached) {
            return Promise.resolve(cached);
        }

        return request({
            url: '/user/info',
        }).then((data) => {
            storage.set(cacheKey, data, cacheStrategy.medium);
            return data;
        });
    },
};

11. 🛠️ 开发工具配置

Vite 配置 (vite.config.js)

import { defineConfig } from 'vite';
import uni from '@dcloudio/vite-plugin-uni';

export default defineConfig({
    plugins: [uni()],
    server: {
        port: 3000,
        open: true,
    },
    build: {
        // 生产环境优化
        minify: 'terser',
        terserOptions: {
            compress: {
                drop_console: true,
                drop_debugger: true,
            },
        },
    },
});

ESLint 配置 (eslint.config.cjs)

module.exports = [
    {
        files: ['src/**/*.{js,vue}'],
        languageOptions: {
            ecmaVersion: 2022,
            sourceType: 'module',
        },
        rules: {
            indent: ['error', 2],
            quotes: ['error', 'single'],
            semi: ['error', 'never'],
            'no-console': 'warn',
            'no-debugger': 'error',
        },
    },
];

12. 📋 开发命令规范

常用命令

# 安装依赖
npm install

# H5开发
npm run dev:h5

# 微信小程序开发
npm run dev:mp-weixin

# App开发
npm run dev:app

# 构建H5
npm run build:h5

# 构建微信小程序
npm run build:mp-weixin

# 代码检查
npm run lint

# 代码自动修复
npm run lint:fix

13. ✅ 最佳实践总结

代码规范

  1. 文件组织: 按功能模块组织文件结构
  2. 组件复用: 提取通用组件,避免重复代码
  3. 样式管理: 使用 CSS 变量系统,便于主题切换
  4. 性能优化: 合理使用缓存和懒加载
  5. 用户体验: 添加加载状态、错误处理和无障碍支持

开发流程

  1. 需求分析 → 确定页面结构和功能
  2. 组件设计 → 拆分可复用组件
  3. 页面开发 → 按模板编写页面代码
  4. 样式调优 → 使用统一的设计规范
  5. 功能测试 → 多平台测试确保兼容性
  6. 性能优化 → 优化加载速度和用户体验

质量保证

  1. 代码审查: 确保代码符合规范
  2. 功能测试: 确保所有功能正常工作
  3. 兼容性测试: 确保多平台兼容
  4. 性能测试: 确保应用性能满足要求
  5. 用户体验测试: 确保交互流畅自然

🎯 总结

本规范基于实际的 Vue 3 + uniapp + uview-plus 项目制定,提供了完整的现代化移动端开发流程。

✨ 技术特色:

  • ✅ Vue 3: 现代化的响应式框架
  • ✅ uview-plus: 成熟的 Vue 3 UI 组件库
  • ✅ 现代 CSS: 使用 CSS 变量的设计系统
  • ✅ 无障碍访问: 完整的无障碍访问支持
  • ✅ 智能缓存: 高效的数据缓存策略
  • ✅ 用户体验: 流畅的动画和手势支持

🎨 设计理念:

  • 现代简约的视觉设计
  • 一致的交互体验
  • 响应式适配
  • 无障碍友好

📱 平台支持:

  • H5 (Web)
  • 微信小程序
  • 支付宝小程序
  • iOS/Android App
  • 鸿蒙 App

遵循本规范能够确保代码质量、提升开发效率,并为用户提供优秀的移动端体验。


📝 维护: 移动端开发规范文档
🔄 更新: 2025-01-03 (新增登录状态管理规范)
📁 文件: .cursor/cursorrules-mobile.md