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

1837 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
### 版本信息
```json
{
"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. 🎨 页面开发规范
### 页面模板标准
```vue
<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 变量系统
```scss
// 主题色彩变量
: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. 🧩 组件开发规范
### 自定义组件模板
```vue
<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 图标**
```vue
<!-- ✅ 推荐:使用 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 组件**
```vue
<!-- ✅ 支持:使用 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. **🔄 动态图标类名**
```vue
<!-- ✅ 动态切换图标 -->
<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`
```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`
```javascript
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`
```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>
```
### 标准使用方法
#### 基础用法
```vue
<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 新增)
#### 🎯 设计原则
移动端列表中的操作按钮统一采用**图标+文字垂直排列**的无边框设计,提升用户体验和界面美观度。
#### 📋 标准按钮模板
```vue
<!-- 标准操作按钮容器 -->
<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>
```
#### 🎨 标准样式定义
```scss
/* 操作按钮容器 */
.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)
```javascript
// 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)
```javascript
// 用户信息缓存工具
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)
```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` 生命周期中检查登录状态:
```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 || '用户';
// 根据需要设置其他用户信息
}
},
},
};
```
## 8. 📱 页面配置规范
### pages.json 配置
```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 配置
```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. 🎭 用户体验规范
### 交互设计规范
#### 动画效果使用
```javascript
// 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();
// 视觉反馈
// 处理点击逻辑
}
}
```
#### 手势操作支持
```javascript
// 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();
}
}
});
},
};
```
### 无障碍访问规范
```javascript
// 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. 🚀 性能优化规范
### 代码优化
#### 组件懒加载
```javascript
// 页面中使用懒加载
export default {
components: {
// 懒加载组件
LazyComponent: () => import('@/components/LazyComponent.vue'),
},
};
```
#### 图片优化
```vue
<template>
<!-- 响应式图片 -->
<image :src="imageSrc" mode="aspectFit" lazy-load :fade-show="true" @error="handleImageError" />
</template>
<script>
export default {
methods: {
handleImageError() {
// 图片加载失败处理
console.log('图片加载失败');
},
},
};
</script>
```
### 网络优化
#### 请求缓存策略
```javascript
// 智能缓存策略
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` 中:
```json
{
"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` 产生配置冲突:
```javascript
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` 仅用于构建优化
✅ **避免冲突**: 不要在两个文件中重复配置同一项目
✅ **多端兼容**: 确保配置在所有平台都生效
### 开发命令规范
```bash
# 安装依赖
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开发中,图标组件统一使用 <u-icon />(uview-ui 2.x 官方标准),同时支持iconfont自定义图标。
- 推荐通过 easycom 自动全局注册,无需手动 import。
- 标准用法示例:
```vue
<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. 组件属性规范
```vue
<!-- ✅ 正确用法 -->
<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. 数据格式要求
```javascript
// ✅ 正确的数据格式
data() {
return {
// 对象数组数据
optionsArray: [
{ label: "选项1", value: "1" },
{ label: "选项2", value: "2" },
{ label: "选项3", value: "3" }
],
// 选中索引(必须是数组格式)
indexArray: [0],
// 控制显示
showPicker: false
}
}
// 在模板中使用时,columns必须包装成二维数组
:columns="[optionsArray]" // 注意中括号包装
```
#### 3. 事件处理规范
```javascript
// ✅ 正确的事件处理
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. 完整使用示例
```vue
<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 新增)
- 移动端弹窗开发统一使用 <u-popup />(uview-ui 2.x 官方标准)
- 推荐通过 easycom 自动全局注册,无需手动 import。
- 标准用法示例:
```vue
<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