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

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