Files
hzdyjs/.cursor/cursorrules-mobile.md
T
2025-08-09 19:54:36 +08:00

48 KiB
Raw Blame History

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

适用范围

  • 项目目录: mobile/ (智慧校园移动应用)
  • 开发语言: JavaScript (Vue 2)
  • 平台支持: H5、小程序、APP、鸿蒙
  • UI 框架: uView UI 2.0.38 (Vue 2 版本)

智慧校园移动端开发规范

1. 🎯 技术栈规范

核心技术

  • 基础框架: uni-app + Vue 2.6.14
  • UI 组件库: uview-ui 2.0.38 (Vue 2 版本)
  • 开发语言: JavaScript (ES6+)
  • 样式预处理: Sass 1.90.0
  • 构建工具: Vue CLI 5.0.8
  • 代码规范: ESLint 5.0.8

版本信息

{
    "name": "mobile2",
    "version": "0.1.0",
    "dependencies": {
        "@dcloudio/uni-app": "^2.0.2-4070520250711001",
        "uview-ui": "2.0.38",
        "vue": ">= 2.6.14 < 2.7",
        "vue-class-component": "^6.3.2",
        "vue-property-decorator": "^8.0.0",
        "vuex": "^3.2.0"
    },
    "devDependencies": {
        "sass": "^1.90.0",
        "vue-template-compiler": ">= 2.6.14 < 2.7",
        "typescript": "~4.5.5"
    }
}

2. 📁 项目结构规范

目录结构

mobile/
├── src/                          # 源码目录
│   ├── components/               # 通用组件
│   │   ├── Card.vue             # 卡片组件
│   │   ├── ServiceItem.vue      # 服务项组件
│   │   └── GridLayout.vue       # 网格布局组件
│   ├── pages/                   # 页面文件
│   │   ├── index/               # 首页
│   │   │   └── index.vue        # 首页内容
│   │   ├── login/               # 登录页
│   │   │   └── index.vue        # 登录页面
│   │   └── task/                # 工作流页面
│   │       ├── myDocument.vue   # 我发起的
│   │       ├── allTaskWaiting.vue # 我的待办
│   │       ├── taskFinish.vue   # 我的已办
│   │       └── taskCopyList.vue # 抄送我的
│   ├── utils/                   # 工具类
│   │   ├── request.js           # 网络请求工具
│   │   ├── user.js              # 用户管理工具
│   │   ├── permission.js        # 权限管理工具
│   │   └── router.js            # 路由管理工具
│   ├── common/                  # 公共模块
│   │   ├── request.js           # API请求封装
│   │   ├── storage.js           # 存储工具
│   │   └── crypto.js            # 加密工具
│   ├── static/                  # 静态资源
│   │   ├── fonts/               # 字体文件
│   │   │   └── iconfont.css     # 自定义图标
│   │   └── images/              # 图片资源
│   ├── App.vue                  # 应用入口
│   ├── main.ts                  # 项目入口
│   ├── pages.json               # 页面配置
│   ├── manifest.json            # 应用配置
│   └── uni.scss                 # 全局样式
├── package.json                 # 项目配置
├── vue.config.js               # Vue CLI配置
└── README.md                   # 项目说明

3. 🎨 页面开发规范

页面模板标准

<template>
    <view class="page-container">
        <!-- 自定义导航栏 -->
        <view class="custom-navbar">
            <text class="navbar-title">{{ pageTitle }}</text>
        </view>

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

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

<script>
export default {
    name: 'PageName',
    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);
        },
    },
};
</script>

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

.custom-navbar {
    background: linear-gradient(135deg, #007aff, #0051d9);
    padding: calc(var(--status-bar-height) + 12px) 16px 16px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.navbar-title {
    font-size: 18px;
    font-weight: 600;
    color: #ffffff;
}

.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;
}

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 (如: Card.vue, ServiceItem.vue)
  • 组件名称: 使用 PascalCase (如: Card, ServiceItem)
  • 引用方式: 支持驼峰和短横线 (如: <Card> 或 <card>)

5. 🎨 图标使用规范

🚨 强制性图标使用规范

⚠️ 重要说明: 移动端项目使用 iconfont 图标系统,同时支持 uview-ui 的 u-icon 组件!

📋 图标使用策略

  1. ✅ 优先使用 iconfont 图标

    <!-- ✅ 推荐:使用 iconfont 图标 -->
    <text class="iconfont icon-search" style="font-size: 20px; color: #666;"></text>
    <text class="iconfont icon-account-circle" style="font-size: 24px;"></text>
    <text class="iconfont icon-close" @click="handleClose"></text>
    
  2. ✅ 支持 uview-ui 的 u-icon 组件

    <!-- ✅ 支持:使用 uview-ui 图标 -->
    <u-icon name="search" size="20" color="#666"></u-icon>
    <u-icon name="account" size="24"></u-icon>
    <u-icon name="close" @click="handleClose"></u-icon>
    
  3. 🔄 动态图标类名

    <!-- ✅ 动态切换图标 -->
    <text 
        :class="'iconfont ' + (expanded ? 'icon-arrow-down' : 'icon-arrow-right')"
        style="font-size: 16px; color: #999;"
        @click="toggleExpand"
    ></text>
    

图标配置文件

位置: 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.ts

import Vue from 'vue'
import App from './App.vue'
import './uni.promisify.adaptor'
import uView from "uview-ui";
Vue.use(uView);
Vue.config.productionTip = false

const app = new (typeof App === 'function' ? App : Vue.extend(Object.assign({ mpType: 'app' }, App)))
app.$mount();

位置: mobile/src/App.vue

<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
    mpType: 'app',
    onLaunch() {
        console.log('App Launch');
    },
    onShow() {
        console.log('App Show');
    },
    onHide() {
        console.log('App Hide');
    },
});
</script>

<style lang="scss">
/* 注意要写在第一行,同时给style标签加入lang="scss"属性 */
@import 'uview-ui/index.scss';

/* 引入自定义图标系统 */
@import '@/static/fonts/iconfont.css';
</style>

标准使用方法

基础用法

<template>
    <!-- iconfont 图标 -->
    <text class="iconfont icon-user"></text>
    <text class="iconfont icon-lock"></text>
    <text class="iconfont icon-search"></text>

    <!-- uview-ui 图标 -->
    <u-icon name="account" size="20" color="#666"></u-icon>
    <u-icon name="lock" size="20" color="#666"></u-icon>
    <u-icon name="search" size="20" color="#666"></u-icon>

    <!-- 在表单中使用 -->
    <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>

6. 📱 移动端UI组件规范

列表操作按钮设计规范 (2025-01-03 新增)

🎯 设计原则

移动端列表中的操作按钮统一采用图标+文字垂直排列的无边框设计,提升用户体验和界面美观度。

📋 标准按钮模板

<!-- 标准操作按钮容器 -->
<view class="card-actions">
    <!-- 查看详细按钮 -->
    <view class="action-btn info-btn" @click="handleView(item)">
        <u-icon name="eye" size="16" color="#3c9cff"></u-icon>
        <text class="btn-text">查看详细</text>
    </view>
    
    <!-- 编辑修改按钮 -->
    <view class="action-btn success-btn" @click="handleEdit(item)">
        <u-icon name="edit-pen" size="16" color="#5ac725"></u-icon>
        <text class="btn-text">编辑</text>
    </view>
    
    <!-- 历史记录按钮 -->
    <view class="action-btn warning-btn" @click="handleHistory(item)">
        <u-icon name="clock" size="16" color="#ff9900"></u-icon>
        <text class="btn-text">历史记录</text>
    </view>
    
    <!-- 删除操作按钮 -->
    <view class="action-btn error-btn" @click="handleDelete(item)">
        <u-icon name="trash" size="16" color="#f56c6c"></u-icon>
        <text class="btn-text">删除</text>
    </view>
</view>

🎨 标准样式定义

/* 操作按钮容器 */
.card-actions {
    display: flex;
    justify-content: space-around;
    padding: 20rpx;
    border-top: 1px solid #f0f0f0;
    gap: 16rpx;
}

/* 统一按钮样式 */
.action-btn {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 16rpx 20rpx;
    border-radius: 12rpx;
    background-color: #f8f9fa;
    min-width: 120rpx;
    transition: all 0.3s ease;
    
    /* 点击动画效果 */
    &:active {
        transform: scale(0.95);
        opacity: 0.8;
    }
}

/* 按钮文字样式 */
.btn-text {
    font-size: 24rpx;
    margin-top: 8rpx;
    color: #303133;
    font-weight: 500;
}

/* 主题色彩方案 */
.info-btn {
    background-color: rgba(60, 156, 255, 0.1);
    .btn-text { color: #3c9cff; }
}

.success-btn {
    background-color: rgba(90, 199, 37, 0.1);
    .btn-text { color: #5ac725; }
}

.warning-btn {
    background-color: rgba(255, 153, 0, 0.1);
    .btn-text { color: #ff9900; }
}

.error-btn {
    background-color: rgba(245, 108, 108, 0.1);
    .btn-text { color: #f56c6c; }
}

.primary-btn {
    background-color: rgba(51, 102, 255, 0.1);
    .btn-text { color: #3366ff; }
}

.default-btn {
    background-color: rgba(144, 147, 153, 0.1);
    .btn-text { color: #909399; }
}

#### 🎨 按钮背景色规范

**📱 移动端按钮背景色设计规范**

**背景色选项**:
- ✅ **完全透明**: `rgba(r, g, b, 0)` - 无背景色,纯文字按钮
- ✅ **半透明背景色**: `rgba(r, g, b, 0.1)` 到 `rgba(r, g, b, 0.3)` - 有背景色的按钮
- ✅ **实色背景**: `rgba(r, g, b, 1)` - 完全实色背景

**推荐使用场景**:
- **完全透明背景**: 适用于简洁设计,强调文字内容
- **半透明背景**: 适用于需要视觉层次的场景
- **实色背景**: 适用于重要操作按钮

**当前项目使用**:
```scss
.info-btn {
    background-color: rgba(60, 156, 255, 0);    // ✅ 完全透明
    .btn-text { color: #3c9cff; }
}

.success-btn {
    background-color: rgba(90, 199, 37, 0);     // ✅ 完全透明
    .btn-text { color: #5ac725; }
}

.warning-btn {
    background-color: rgba(255, 153, 0, 0);     // ✅ 完全透明
    .btn-text { color: #ff9900; }
}

设计原则:

  • 根据设计需求选择合适的背景色
  • 确保按钮文字清晰可读
  • 保持整体视觉一致性
  • 符合项目设计风格

#### 🔧 图标选择规范

| 功能类型 | 推荐图标 | 颜色 | 样式类 | 使用场景 |
|---------|---------|------|--------|----------|
| 查看详细 | `eye` | #3c9cff | info-btn | 查看、预览、详情 |
| 编辑修改 | `edit-pen` | #5ac725 | success-btn | 编辑、修改、更新 |
| 历史记录 | `clock` | #ff9900 | warning-btn | 时间、历史、记录 |
| 删除操作 | `trash` | #f56c6c | error-btn | 删除、移除、清空 |
| 下载导出 | `download` | #3c9cff | info-btn | 下载、导出、保存 |
| 上传提交 | `upload` | #5ac725 | success-btn | 上传、提交、发送 |
| 审核通过 | `checkmark` | #5ac725 | success-btn | 通过、确认、同意 |
| 审核拒绝 | `close` | #f56c6c | error-btn | 拒绝、关闭、取消 |
| 详情信息 | `info-circle` | #3c9cff | info-btn | 信息、说明、帮助 |
| 设置配置 | `setting` | #909399 | default-btn | 设置、配置、管理 |
| 分享转发 | `share` | #3366ff | primary-btn | 分享、转发、推送 |
| 收藏点赞 | `heart` | #f56c6c | error-btn | 收藏、点赞、喜欢 |

#### 💡 使用指导

1. **✅ 强制使用场景**
   - 所有移动端列表页面的操作按钮
   - 卡片组件中的操作按钮
   - 表格行操作按钮

2. **🎨 设计要求**
   - 图标大小统一为16px
   - 文字大小统一为24rpx(12px)
   - 按钮最小宽度120rpx
   - 圆角半径12rpx

3. **🔄 交互要求**
   - 必须包含点击动画效果(:active伪类)
   - 缩放比例0.95,透明度0.8
   - 过渡动画时间0.3s

4. **📏 布局要求**
   - 使用flex布局,space-around分布
   - 按钮间距16rpx
   - 容器内边距20rpx
   - 顶部边框分割线

#### 🚨 注意事项

- **禁止使用传统边框按钮**: 移动端列表不再使用`<u-button>`组件
- **图标组件规范**: 统一使用`<u-icon />`组件
- **颜色主题一致**: 严格按照表格中的颜色方案
- **响应式适配**: 确保在不同屏幕尺寸下正常显示
- **无障碍访问**: 文字描述清晰,图标语义明确

#### 📝 完整示例

```vue
<template>
    <view class="list-container">
        <!-- 列表项 -->
        <view class="list-item" v-for="item in dataList" :key="item.id">
            <!-- 主要内容 -->
            <view class="item-content">
                <text class="item-title">{{ item.title }}</text>
                <text class="item-desc">{{ item.description }}</text>
            </view>
            
            <!-- 操作按钮 -->
            <view class="card-actions">
                <view class="action-btn info-btn" @click="handleView(item)">
                    <u-icon name="eye" size="16" color="#3c9cff"></u-icon>
                    <text class="btn-text">查看详细</text>
                </view>
                
                <view v-if="item.canEdit" class="action-btn success-btn" @click="handleEdit(item)">
                    <u-icon name="edit-pen" size="16" color="#5ac725"></u-icon>
                    <text class="btn-text">编辑</text>
                </view>
                
                <view class="action-btn warning-btn" @click="handleHistory(item)">
                    <u-icon name="clock" size="16" color="#ff9900"></u-icon>
                    <text class="btn-text">历史记录</text>
                </view>
            </view>
        </view>
    </view>
</template>

<script>
export default {
    data() {
        return {
            dataList: [
                { id: 1, title: '示例标题1', description: '示例描述', canEdit: true },
                { id: 2, title: '示例标题2', description: '示例描述', canEdit: false }
            ]
        }
    },
    methods: {
        handleView(item) {
            console.log('查看详细:', item);
            // 跳转到详情页面
        },
        handleEdit(item) {
            console.log('编辑项目:', item);
            // 跳转到编辑页面
        },
        handleHistory(item) {
            console.log('查看历史:', item);
            // 跳转到历史页面
        }
    }
}
</script>

<style lang="scss" scoped>
.list-container {
    padding: 20rpx;
}

.list-item {
    background-color: #ffffff;
    border-radius: 12rpx;
    margin-bottom: 20rpx;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}

.item-content {
    padding: 30rpx 30rpx 20rpx;
}

.item-title {
    font-size: 32rpx;
    font-weight: 600;
    color: #303133;
    margin-bottom: 10rpx;
}

.item-desc {
    font-size: 28rpx;
    color: #606266;
}

/* 引入标准按钮样式 */
.card-actions {
    display: flex;
    justify-content: space-around;
    padding: 20rpx;
    border-top: 1px solid #f0f0f0;
    gap: 16rpx;
}

.action-btn {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 16rpx 20rpx;
    border-radius: 12rpx;
    background-color: #f8f9fa;
    min-width: 120rpx;
    transition: all 0.3s ease;
    
    &:active {
        transform: scale(0.95);
        opacity: 0.8;
    }
}

.btn-text {
    font-size: 24rpx;
    margin-top: 8rpx;
    color: #303133;
    font-weight: 500;
}

.info-btn {
    background-color: rgba(60, 156, 255, 0.1);
    .btn-text { color: #3c9cff; }
}

.success-btn {
    background-color: rgba(90, 199, 37, 0.1);
    .btn-text { color: #5ac725; }
}

.warning-btn {
    background-color: rgba(255, 153, 0, 0.1);
    .btn-text { color: #ff9900; }
}
</style>

7. 🌐 API 接口规范

网络请求工具 (common/request.js)

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

// 统一请求方法
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',
        }),
};

用户信息缓存工具 (common/storage.js)

// 用户信息缓存工具
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);
        }
    },
};

7. 🔐 登录状态管理规范

登录页面开发规范

登录页面模板 (pages/login/index.vue)

<template>
    <view class="login-bg">
        <view class="login-card">
            <!-- Logo和标题区域 -->
            <view class="login-header">
                <view class="login-logo">
                    <image class="logo-img" src="/static/logo-2.png" mode="aspectFit" />
                </view>
                <view class="login-text">
                    <view class="login-title">智慧校园</view>
                    <view class="login-subtitle">杭州第一技师学院</view>
                </view>
            </view>
            <!-- 登录表单 -->
            <view class="login-form">
                <!-- 账号输入框 -->
                <view class="input-wrapper">
                    <u-input
                        v-model="username"
                        placeholder="教工号/学号"
                        type="text"
                        border="true"
                        :custom-style="inputStyle"
                        placeholder-style="color: #999; font-size: 16px"
                    >
                        <template #prefix>
                            <u-icon
                                name="account"
                                size="20"
                                color="#b0b8c9"
                                class="input-prefix-icon"
                            />
                        </template>
                    </u-input>
                </view>
                <!-- 密码输入框 -->
                <view class="input-wrapper">
                    <u-input
                        v-model="password"
                        placeholder="密码"
                        type="password"
                        border="true"
                        :custom-style="inputStyle"
                        placeholder-style="color: #999; font-size: 16px"
                        @confirm="onPasswordConfirm"
                    >
                        <template #prefix>
                            <u-icon
                                name="lock"
                                size="20"
                                color="#b0b8c9"
                                class="input-prefix-icon"
                            />
                        </template>
                    </u-input>
                </view>
                <!-- 登录按钮 -->
                <u-button
                    type="primary"
                    :custom-style="buttonStyle"
                    :loading="loading"
                    :disabled="loading"
                    @click="onLogin"
                >
                    <view class="login-btn-content" v-if="!loading">
                        <text class="login-btn-text">登录</text>
                    </view>
                    <view class="login-btn-content" v-else>
                        <text class="login-btn-text">登录中...</text>
                    </view>
                </u-button>
            </view>
            <!-- 版权信息 -->
            <view class="login-copyright">&copy; 2025 杭州第一技师学院 All Rights Reserved.</view>
        </view>
    </view>
</template>

<script>
import { api } from '@/common/request';
import { userStorage } from '@/common/storage';

export default {
    name: 'LoginPage',
    data() {
        return {
            username: '',
            password: '',
            loading: 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',
            },
        };
    },
    onLoad() {
        // 检查是否已登录
        if (userStorage.isLoggedIn()) {
            this.redirectToHome();
        }
    },
    methods: {
        async onLogin() {
            if (!this.username || !this.password) {
                uni.showToast({
                    title: '请输入用户名和密码',
                    icon: 'none',
                });
                return;
            }

            this.loading = true;

            try {
                const loginResponse = await api.login({
                    username: this.username,
                    password: this.password,
                });

                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;
            }
        },

        onPasswordConfirm() {
            this.onLogin();
        },

        redirectToHome() {
            uni.reLaunch({ url: '/pages/index/index' });
        },
    },
};
</script>

<style lang="scss" scoped>
.login-bg {
    min-height: 100vh;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
}

.login-card {
    background: rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(20px);
    border-radius: 20px;
    padding: 40px 30px;
    width: 100%;
    max-width: 400px;
    border: 1px solid rgba(255, 255, 255, 0.2);
}

.login-header {
    text-align: center;
    margin-bottom: 40px;
}

.login-logo {
    margin-bottom: 20px;
}

.logo-img {
    width: 80px;
    height: 80px;
    border-radius: 15px;
}

.login-title {
    font-size: 28px;
    font-weight: bold;
    color: #ffffff;
    margin-bottom: 8px;
}

.login-subtitle {
    font-size: 16px;
    color: rgba(255, 255, 255, 0.8);
}

.login-form {
    margin-bottom: 30px;
}

.input-wrapper {
    margin-bottom: 20px;
}

.login-btn-content {
    display: flex;
    align-items: center;
    justify-content: center;
}

.login-btn-text {
    color: #ffffff;
    font-size: 18px;
    font-weight: bold;
}

.login-copyright {
    text-align: center;
    font-size: 12px;
    color: rgba(255, 255, 255, 0.6);
    margin-top: 20px;
}
</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 || '用户';
                // 根据需要设置其他用户信息
            }
        },
    },
};

8. 📱 页面配置规范

pages.json 配置

{
    "easycom": {
        "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
    },
    "pages": [
        {
            "path": "pages/login/index",
            "style": {
                "navigationBarTitleText": "登录",
                "navigationStyle": "custom"
            }
        },
        {
            "path": "pages/index/index",
            "style": {
                "navigationBarTitleText": "杭州第一技师学院智慧校园",
                "navigationStyle": "custom"
            }
        },
        {
            "path": "pages/task/myDocument",
            "style": {
                "navigationBarTitleText": "我发起的",
                "navigationBarBackgroundColor": "#5d87ff",
                "navigationBarTextStyle": "white"
            }
        },
        {
            "path": "pages/task/allTaskWaiting",
            "style": {
                "navigationBarTitleText": "我的待办",
                "navigationBarBackgroundColor": "#5d87ff",
                "navigationBarTextStyle": "white"
            }
        },
        {
            "path": "pages/task/taskFinish",
            "style": {
                "navigationBarTitleText": "我的已办",
                "navigationBarBackgroundColor": "#5d87ff",
                "navigationBarTextStyle": "white"
            }
        },
        {
            "path": "pages/task/taskCopyList",
            "style": {
                "navigationBarTitleText": "抄送我的",
                "navigationBarBackgroundColor": "#5d87ff",
                "navigationBarTextStyle": "white"
            }
        }
    ],
    "globalStyle": {
        "navigationBarTextStyle": "black",
        "navigationBarTitleText": "智慧校园",
        "navigationBarBackgroundColor": "#5d87ff",
        "backgroundColor": "#F8F8F8"
    }
}

manifest.json 配置

{
    "name": "mobile2",
    "appid": "",
    "description": "",
    "versionName": "1.0.0",
    "versionCode": "100",
    "transformPx": false,
    "app-plus": {
        "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": {}
        },
        "usingComponents": true
    },
    "quickapp": {},
    "mp-weixin": {
        "usingComponents": true,
        "appid": "",
        "setting": {
            "urlCheck": true
        }
    },
    "mp-alipay": {
        "usingComponents": true
    },
    "mp-baidu": {
        "usingComponents": true
    },
    "mp-toutiao": {
        "usingComponents": true
    },
    "mp-qq": {
        "usingComponents": true
    }
}

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. 🛠️ 开发工具配置

配置文件管理规范

manifest.json 配置 (推荐)

⚠️ 重要: uni-app 项目中,开发服务器配置应统一在 manifest.json 的 h5.devServer 中:

{
    "h5": {
        "devServer": {
            "https": false,
            "allowedHosts": "all",
            "port": 8081,
            "host": "0.0.0.0",
            "proxy": {
                "/api": {
                    "target": "http://localhost:8080",
                    "changeOrigin": true,
                    "pathRewrite": {
                        "^/api": ""
                    }
                }
            }
        },
        "publicPath": "./",
        "router": {
            "mode": "hash"
        },
        "title": "智慧校园移动端"
    }
}

Vue CLI 配置 (vue.config.js)

🔧 仅用于构建优化,避免与 manifest.json 产生配置冲突:

module.exports = {
    transpileDependencies: ['uview-ui'],
    configureWebpack: {
        resolve: {
            alias: {
                '@': require('path').resolve(__dirname, 'src'),
            },
        },
    },
};

配置优先级说明

uni-app 配置优先级:

  • H5平台: manifest.json > vue.config.js
  • 小程序平台: 仅使用 manifest.json
  • App平台: 仅使用 manifest.json

最佳实践: ✅ 统一配置: 开发服务器配置统一在 manifest.json 中
✅ 构建工具: vue.config.js 仅用于构建优化
✅ 避免冲突: 不要在两个文件中重复配置同一项目
✅ 多端兼容: 确保配置在所有平台都生效

开发命令规范

# 安装依赖
npm install

# H5开发
npm run dev:h5

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

# App开发
npm run dev:app-plus

# 构建H5
npm run build:h5

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

# 构建App
npm run build:app-plus

12. ✅ 最佳实践总结

代码规范

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

开发流程

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

质量保证

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

🎯 总结

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

✨ 技术特色:

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

🎨 设计理念:

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

📱 平台支持:

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

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

13. 🔧 uView 2.0 组件使用规范

uview-ui 图标组件规范(2025-01-03 更新)

  • 移动端UI开发中,图标组件统一使用 (uview-ui 2.x 官方标准),同时支持iconfont自定义图标。
  • 推荐通过 easycom 自动全局注册,无需手动 import。
  • 标准用法示例:
    <u-icon name="account" size="22" color="#b0b8c9" />
    <u-icon name="lock-open" size="22" color="#b0b8c9" />
    
  • 图标名称请参考 uview-ui 官方文档:https://www.uviewui.com/components/icon.html

uview-ui 选择器组件规范(2025-01-03 新增)

⚠️ 重要:u-picker组件必须严格按照以下规范使用,否则会出现数据显示空白问题!

1. 组件属性规范

<!-- ✅ 正确用法 -->
<u-picker
    :columns="[optionsArray]"    <!-- 必须是二维数组格式 -->
    keyName="label"              <!-- 指定显示字段名 -->
    v-model="indexArray"         <!-- 选中项索引数组 -->
    :show="showPicker"           <!-- 控制显示/隐藏 -->
    @confirm="onConfirm"         <!-- 确认事件 -->
    @close="showPicker = false"  <!-- 关闭事件 -->
></u-picker>

<!-- ❌ 错误用法(旧版本写法)-->
<u-picker
    :range="optionsArray"        <!-- 错误:应该用columns -->
    range-key="label"            <!-- 错误:应该用keyName -->
    @change="onChange"           <!-- 错误:应该用confirm -->
></u-picker>

2. 数据格式要求

// ✅ 正确的数据格式
data() {
    return {
        // 对象数组数据
        optionsArray: [
            { label: "选项1", value: "1" },
            { label: "选项2", value: "2" },
            { label: "选项3", value: "3" }
        ],
        // 选中索引(必须是数组格式)
        indexArray: [0],
        // 控制显示
        showPicker: false
    }
}

// 在模板中使用时,columns必须包装成二维数组
:columns="[optionsArray]"  // 注意中括号包装

3. 事件处理规范

// ✅ 正确的事件处理
onConfirm(e) {
    console.log('picker选择结果:', e);
    const index = e.value[0];        // uview2格式:e.value
    const selectedItem = this.optionsArray[index];
    this.form.fieldName = selectedItem.value;
    this.indexArray = [index];
    this.showPicker = false;
}

// ❌ 错误的事件处理
onConfirm(e) {
    const index = e.detail.value[0]; // 错误:小程序原生格式
}

4. 完整使用示例

<template>
    <view>
        <!-- 显示区域 -->
        <view class="picker-input" @click="showPicker = true">
            {{ getSelectedLabel() || '请选择' }}
        </view>
        
        <!-- picker组件 -->
        <u-picker
            :columns="[optionsArray]"
            keyName="label"
            v-model="indexArray"
            :show="showPicker"
            @confirm="onConfirm"
            @close="showPicker = false"
        ></u-picker>
    </view>
</template>

<script>
export default {
    data() {
        return {
            optionsArray: [
                { label: "汉族", value: "01" },
                { label: "蒙古族", value: "02" },
                { label: "回族", value: "03" }
            ],
            indexArray: [0],
            showPicker: false,
            form: {
                nationality: ''
            }
        }
    },
    methods: {
        onConfirm(e) {
            const index = e.value[0];
            this.indexArray = [index];
            this.form.nationality = this.optionsArray[index]?.value || '';
            this.showPicker = false;
        },
        getSelectedLabel() {
            const index = this.indexArray[0];
            return this.optionsArray[index]?.label || '';
        }
    }
}
</script>

5. 常见错误及解决

  • 问题1: picker弹出后数据区域空白

    • 原因: 使用了:range而非:columns,或数据格式不是二维数组
    • 解决: 改用:columns="[dataArray]"格式
  • 问题2: 事件处理获取不到正确数据

    • 原因: 使用了e.detail.value而非e.value
    • 解决: 改用e.value[0]获取索引
  • 问题3: 无法显示中文选项文本

    • 原因: 未指定keyName属性
    • 解决: 添加keyName="label"指定显示字段
  • 详细文档参考:https://www.uviewui.com/components/picker.html

uview-ui 弹窗组件规范(2025-01-03 新增)

  • 移动端弹窗开发统一使用 (uview-ui 2.x 官方标准)
  • 推荐通过 easycom 自动全局注册,无需手动 import。
  • 标准用法示例:
    <u-popup v-model="show" mode="bottom" height="500">
        <view class="popup-content">
            <!-- 弹窗内容 -->
        </view>
    </u-popup>
    
  • 常用属性:
    • v-model: 控制弹窗显示/隐藏
    • mode: 弹出方向(bottom、top、left、right、center)
    • height: 弹窗高度(bottom/top模式)
    • width: 弹窗宽度(left/right模式)
    • border-radius: 圆角大小
    • closeable: 是否显示关闭图标
    • mask-close: 是否允许点击遮罩关闭
  • 详细文档参考:https://www.uviewui.com/components/popup.html

📝 维护: 移动端开发规范文档
🔄 更新: 2025-01-03 (调整为Vue 2 + uview-ui技术栈,新增弹窗组件规范)
📁 文件: .cursor/cursorrules-mobile.md