✨ 新增功能: - 全局路由守卫模块 (route-guard.js) - 多层次权限验证 (登录状态+Token存在+Token过期检查) - 自动拦截机制 (页面级+跳转级+HTTP级+应用级) - 401响应自动处理和跳转 - 路由守卫测试页面 🔧 技术实现: - checkPageAccess() 全局页面权限检查 - guardedNavigate/guardedSwitchTab 带权限检查的跳转 - handleUnauthorized() HTTP拦截器401处理 - 公开页面配置管理 - 优雅的错误提示和跳转逻辑 📱 页面集成: - App.vue 初始化路由守卫 - profile/workbench 页面添加权限检查 - index 页面优化跳转逻辑 - 新增测试页面验证功能 📚 文档完善: - 移动端路由守卫使用指南 - 详细的变更记录和技术说明 - 最佳实践和注意事项 🎯 核心效果: 用户登录失效或未登录时,访问任何需要权限的页面都自动跳转到登录页,提升安全性和用户体验。
1787 lines
45 KiB
Markdown
1787 lines
45 KiB
Markdown
# 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
|