51 KiB
51 KiB
Cursor Rules - 移动端开发规范 (Vue 3 + uniapp + uview-plus)
适用范围
- 项目目录:
mobile/(现代化智慧校园移动应用) - 开发语言: JavaScript (Vue 3)
- 平台支持: H5、小程序、APP、鸿蒙
- UI 框架: uView UI 2.x (uview-plus 3.4.45)
智慧校园移动端开发规范
1. 🎯 技术栈规范
核心技术
- 基础框架: uni-app + Vue 3.4.21
- UI 组件库: uview-plus 3.4.45 (Vue 3 版本)
- 开发语言: JavaScript (ES6+)
- 国际化: vue-i18n 9.1.9 (Vue 3 版本)
- 样式预处理: Sass 1.89.2
- 构建工具: Vite 5.2.8
- 代码规范: ESLint 9.30.1
版本信息
{
"name": "uni-preset-vue",
"version": "0.0.0",
"dependencies": {
"@dcloudio/uni-app": "3.0.0-4060620250520001",
"uview-plus": "^3.4.45",
"vue": "^3.4.21",
"vue-i18n": "^9.1.9"
},
"devDependencies": {
"sass": "^1.89.2",
"vite": "5.2.8",
"eslint": "^9.30.1"
}
}
2. 📁 项目结构规范
目录结构
mobile/
├── src/ # 源码目录
│ ├── components/ # 通用组件
│ │ ├── NavBar.vue # 导航栏组件
│ │ ├── Card.vue # 卡片组件
│ │ ├── ServiceItem.vue # 服务项组件
│ │ ├── StatCard.vue # 统计卡片组件
│ │ └── GridLayout.vue # 网格布局组件
│ ├── pages/ # 页面文件
│ │ ├── index/ # 首页
│ │ │ └── index.vue # 首页内容
│ │ ├── workbench/ # 办公中心
│ │ │ └── index.vue # 办公中心内容
│ │ └── profile/ # 我的页面
│ │ └── index.vue # 个人中心内容
│ ├── utils/ # 工具类
│ │ ├── request.js # 网络请求工具
│ │ ├── sse.js # SSE实时数据工具
│ │ ├── device.js # 设备检测工具
│ │ ├── animations.js # 动画工具
│ │ ├── gestures.js # 手势识别工具
│ │ └── accessibility.js # 无障碍访问工具
│ ├── static/ # 静态资源
│ │ ├── tabbar/ # 底部导航图标
│ │ └── fonts/ # 字体文件
│ ├── App.vue # 应用入口
│ ├── main.js # 项目入口
│ ├── pages.json # 页面配置
│ ├── manifest.json # 应用配置
│ └── uni.scss # 全局样式
├── package.json # 项目配置
├── vite.config.js # Vite构建配置
├── eslint.config.cjs # ESLint配置
└── README.md # 项目说明
3. 🎨 页面开发规范
页面模板标准
<template>
<view class="page-container">
<!-- 自定义导航栏 -->
<NavBar
:title="pageTitle"
:show-refresh="true"
:show-search="true"
@refresh="handleRefresh"
@search="handleSearch"
/>
<!-- 主要内容区域 -->
<scroll-view class="main-content" scroll-y enhanced :show-scrollbar="false">
<!-- 页面内容 -->
<view class="content">
<!-- 使用uView-plus组件 -->
<u-cell-group>
<u-cell title="示例项目"></u-cell>
</u-cell-group>
</view>
<!-- 底部间距 -->
<view class="bottom-spacer"></view>
</scroll-view>
</view>
</template>
<script>
import NavBar from '@/components/NavBar.vue';
export default {
name: 'PageName',
components: {
NavBar,
},
data() {
return {
pageTitle: '页面标题',
// 页面数据
};
},
onLoad() {
// 页面加载逻辑
this.initPage();
},
onShow() {
// 页面显示逻辑
},
onUnload() {
// 页面卸载时清理定时器等
if (this.timer) {
clearInterval(this.timer);
}
},
methods: {
initPage() {
// 初始化页面逻辑
},
handleRefresh() {
// 刷新逻辑
uni.showLoading({ title: '刷新中...' });
setTimeout(() => {
uni.hideLoading();
uni.showToast({ title: '刷新成功', icon: 'success' });
}, 1000);
},
handleSearch() {
// 搜索逻辑
},
},
};
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: var(--background-color);
display: flex;
flex-direction: column;
}
.main-content {
flex: 1;
padding-bottom: calc(50px + env(safe-area-inset-bottom));
}
.content {
padding: var(--spacing-md);
}
.bottom-spacer {
height: 20px;
}
</style>
页面样式规范
使用 CSS 变量系统
// 主题色彩变量
:root {
--primary-color: rgb(93, 135, 255);
--primary-light: rgba(93, 135, 255, 0.1);
--primary-dark: rgb(73, 115, 235);
--success-color: #52c41a;
--warning-color: #faad14;
--error-color: #ff4d4f;
--text-primary: #262626;
--text-secondary: #595959;
--text-disabled: #bfbfbf;
--border-color: #f0f0f0;
--background-color: #f8f9fa;
--white: #ffffff;
--shadow-light: 0 2px 8px rgba(0, 0, 0, 0.06);
--shadow-medium: 0 4px 16px rgba(0, 0, 0, 0.12);
--radius-small: 4px;
--radius-medium: 8px;
--radius-large: 12px;
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--transition-fast: 0.2s ease;
--transition-medium: 0.3s ease;
--font-size-xs: 12px;
--font-size-sm: 14px;
--font-size-md: 16px;
--font-size-lg: 18px;
--font-size-xl: 20px;
}
工具类规范
/* 布局工具类 */
.flex {
display: flex;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.flex-column {
display: flex;
flex-direction: column;
}
/* 文本工具类 */
.text-center {
text-align: center;
}
.text-primary {
color: var(--primary-color);
}
.text-secondary {
color: var(--text-secondary);
}
/* 背景工具类 */
.bg-primary {
background-color: var(--primary-color);
}
.bg-white {
background-color: var(--white);
}
/* 间距工具类 */
.mt-sm {
margin-top: var(--spacing-sm);
}
.mt-md {
margin-top: var(--spacing-md);
}
.mt-lg {
margin-top: var(--spacing-lg);
}
.p-sm {
padding: var(--spacing-sm);
}
.p-md {
padding: var(--spacing-md);
}
.p-lg {
padding: var(--spacing-lg);
}
4. 🧩 组件开发规范
自定义组件模板
<template>
<view class="component-wrapper" :class="computedClass">
<!-- 组件内容 -->
<slot></slot>
</view>
</template>
<script>
/**
* 智慧校园通用组件
* @property {String} customClass 自定义样式类
* @property {Boolean} disabled 是否禁用
* @example <custom-component :disabled="false">内容</custom-component>
*/
export default {
name: 'CustomComponent',
props: {
customClass: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
},
computed: {
computedClass() {
return {
disabled: this.disabled,
[this.customClass]: !!this.customClass,
};
},
},
methods: {
handleClick() {
if (this.disabled) return;
this.$emit('click');
// 触觉反馈
uni.vibrateShort();
},
},
};
</script>
<style lang="scss" scoped>
.component-wrapper {
// 组件样式
transition: var(--transition-fast);
&.disabled {
opacity: 0.5;
pointer-events: none;
}
}
</style>
组件命名规范
- 文件命名: 使用 PascalCase (如:
NavBar.vue,ServiceItem.vue) - 组件名称: 使用 PascalCase (如:
NavBar,ServiceItem) - 引用方式: 支持驼峰和短横线 (如:
<NavBar>或<nav-bar>)
5. 🎨 图标使用规范 (SVG Data URI 方案)
🚨 强制性图标使用规范(2025-08-06 新增)
⚠️ 重要说明: 移动端项目已全面禁用 uview-plus 的 u-icon 组件,强制使用 iconfont 图标系统!
📋 强制要求
-
🚫 禁止使用 u-icon 组件
<!-- ❌ 禁止这样写 --> <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> -
✅ 必须使用 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> -
🔄 动态图标类名
<!-- ✅ 动态切换图标 --> <text :class="'iconfont ' + (expanded ? 'icon-arrow-down' : 'icon-arrow-right')" style="font-size: 16px; color: #999;" @click="toggleExpand" ></text>
🎯 替换映射表
| u-icon name | iconfont class | 说明 |
|---|---|---|
search |
icon-search |
搜索图标 |
account |
icon-account-circle |
用户账户图标 |
close |
icon-close |
关闭图标 |
email |
icon-email |
邮件图标 |
arrow-right |
icon-arrow-right |
右箭头图标 |
arrow-down |
icon-arrow-down |
下箭头图标 |
home |
icon-home |
首页图标 |
setting |
icon-setting |
设置图标 |
背景说明
由于 uview-plus 的 u-icon 组件在某些环境下存在显示异常(显示为横线),项目采用 SVG Data URI 方案实现图标系统,确保跨平台兼容性和显示稳定性。
核心优势
- ✅ 零外部依赖: 图标直接嵌入 CSS,无需字体文件或 CDN
- ✅ 100%兼容性: 支持 H5、小程序、App、HarmonyOS 等所有平台
- ✅ 加载稳定: 无 404 错误,无网络请求,性能优异
- ✅ 样式灵活: 通过 CSS filter 实现颜色自定义
- ✅ 维护简单: 基于 Material Design,图标清晰美观
- ✅ 显示稳定: 彻底解决 u-icon 组件显示异常问题
图标配置文件
位置: mobile/src/static/fonts/iconfont.css
.iconfont {
display: inline-block;
width: 16px;
height: 16px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
/* 用户图标 */
.icon-user {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z'/%3E%3C/svg%3E");
}
/* 密码锁图标 */
.icon-lock {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6z'/%3E%3C/svg%3E");
}
/* 更多图标... */
全局引入配置
位置: mobile/src/main.js
import { createSSRApp } from 'vue';
import App from './App.vue';
import uviewPlus from 'uview-plus';
// 导入 uview-plus 样式
import 'uview-plus/index.scss';
// 导入 iconfont 样式 (必需)
import '@/static/fonts/iconfont.css';
export function createApp() {
const app = createSSRApp(App);
app.use(uviewPlus);
return { app };
}
可用图标列表
基础图标 (4 个)
icon-user- 用户图标icon-lock- 密码锁图标icon-eye- 眼睛图标(显示密码)icon-eye-close- 眼睛关闭图标(隐藏密码)
操作图标 (5 个)
icon-login- 登录图标icon-search- 搜索图标icon-add- 添加图标icon-check- 确认图标icon-close- 关闭图标
导航图标 (4 个)
icon-home- 首页图标icon-message- 消息图标icon-setting- 设置图标icon-arrow-right- 右箭头图标
联系图标 (2 个)
icon-phone- 电话图标icon-mail- 邮箱图标
标准使用方法
基础用法
<template>
<!-- 基础图标 -->
<text class="iconfont icon-user"></text>
<text class="iconfont icon-lock"></text>
<text class="iconfont icon-search"></text>
<!-- 在表单中使用 -->
<view class="input-group">
<text class="iconfont icon-user input-icon"></text>
<input class="login-input" placeholder="账号" />
</view>
<!-- 在按钮中使用 -->
<button class="btn-primary">
<text class="iconfont icon-login btn-icon"></text>
<text>登录</text>
</button>
</template>
自定义样式
<template>
<!-- 自定义大小 -->
<text class="iconfont icon-home large-icon"></text>
<!-- 自定义颜色 -->
<text class="iconfont icon-message primary-icon"></text>
<!-- 内联样式 -->
<text
class="iconfont icon-setting"
:style="{
width: '24px',
height: '24px',
filter: 'brightness(0) saturate(100%) invert(38%) sepia(87%) saturate(6400%) hue-rotate(225deg) brightness(104%) contrast(104%)',
}"
></text>
</template>
<style lang="scss" scoped>
/* 大尺寸图标 */
.large-icon {
width: 24px;
height: 24px;
}
/* 主题色图标 #5d87ff */
.primary-icon {
filter: brightness(0) saturate(100%) invert(38%) sepia(87%) saturate(6400%) hue-rotate(225deg)
brightness(104%) contrast(104%);
}
/* 灰色图标 #b0b8c9 */
.gray-icon {
filter: brightness(0) saturate(100%) invert(67%) sepia(8%) saturate(1179%) hue-rotate(195deg)
brightness(95%) contrast(88%);
}
/* 输入框图标定位 */
.input-icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
width: 18px;
height: 18px;
z-index: 2;
}
/* 按钮图标 */
.btn-icon {
width: 16px;
height: 16px;
margin-right: 8px;
}
</style>
常用颜色 Filter 值
/* 主题色 #5d87ff */
.icon-primary {
filter: brightness(0) saturate(100%) invert(38%) sepia(87%) saturate(6400%) hue-rotate(225deg)
brightness(104%) contrast(104%);
}
/* 成功色 #52c41a */
.icon-success {
filter: brightness(0) saturate(100%) invert(56%) sepia(91%) saturate(2063%) hue-rotate(88deg)
brightness(118%) contrast(84%);
}
/* 警告色 #faad14 */
.icon-warning {
filter: brightness(0) saturate(100%) invert(75%) sepia(95%) saturate(1978%) hue-rotate(7deg)
brightness(108%) contrast(92%);
}
/* 错误色 #ff4d4f */
.icon-error {
filter: brightness(0) saturate(100%) invert(27%) sepia(91%) saturate(2870%) hue-rotate(353deg)
brightness(104%) contrast(97%);
}
/* 灰色 #8c8c8c */
.icon-gray {
filter: brightness(0) saturate(100%) invert(55%) sepia(0%) saturate(0%) hue-rotate(174deg)
brightness(94%) contrast(86%);
}
扩展新图标
当需要添加新图标时,按以下步骤操作:
- 获取 SVG 源码:从 Material Design Icons 或其他图标库获取 SVG
- URL 编码:将 SVG 代码进行 URL 编码
- 添加 CSS 类:在
iconfont.css中添加新的图标类
/* 新图标示例 */
.icon-new {
background-image: url('data:image/svg+xml;charset=utf8,{URL编码的SVG代码}');
}
强制性规范要求
- 禁止使用 uview-plus 的 u-icon 组件:统一使用 SVG Data URI 方案
- 图标类名规范:必须使用
icon-前缀 + 语义化名称 - 基础类必须添加:所有图标元素必须包含
iconfont基础类 - 大小自定义:通过
width和height属性设置图标大小 - 颜色自定义:通过 CSS
filter属性实现颜色变换
性能优化建议
- CSS 压缩:生产环境下压缩
iconfont.css文件 - 按需加载:仅引入项目实际使用的图标
- 缓存策略:CSS 文件设置长期缓存
- 图标复用:相同功能使用统一图标,避免重复定义
6. 🌐 API 接口规范
网络请求工具 (utils/request.js)
// API请求配置
const baseURL =
process.env.NODE_ENV === 'development'
? 'http://localhost:3000/api'
: 'https://your-api-domain.com/api';
// 统一请求方法
export const request = (options) => {
return new Promise((resolve, reject) => {
uni.request({
url: baseURL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: {
'Content-Type': 'application/json',
Authorization: getToken(),
...options.header,
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data);
} else {
reject(res);
}
},
fail: (error) => {
reject(error);
},
});
});
};
// 获取token
function getToken() {
try {
const token = uni.getStorageSync('token');
return token ? `Bearer ${token}` : '';
} catch (e) {
return '';
}
}
// API方法封装
export const api = {
// 用户登录
login: (data) =>
request({
url: '/auth/login',
method: 'POST',
data: {
tenantId: data.tenantId || '000000',
username: data.username,
password: data.password,
clientId: data.clientId || 'e5cd7e4891bf95d1d19206ce24a7b32e',
grantType: data.grantType || 'password',
},
header: {
isToken: false,
isEncrypt: true,
repeatSubmit: false,
},
}),
// 获取用户信息
getUserInfo: () =>
request({
url: '/system/user/getInfo',
method: 'GET',
}),
// 获取租户列表
getTenantList: (isToken = false) =>
request({
url: '/auth/tenant/list',
method: 'GET',
header: {
isToken: isToken,
},
}),
// 注销登录
logout: () =>
request({
url: '/auth/logout',
method: 'POST',
}),
};
// 用户信息缓存工具
export const userStorage = {
// 保存登录信息
saveLoginInfo: (loginResult, userInfo) => {
try {
// 保存token
uni.setStorageSync('access_token', loginResult.access_token);
// 保存用户信息
if (userInfo) {
uni.setStorageSync('userInfo', JSON.stringify(userInfo));
}
// 保存登录状态
uni.setStorageSync('isLoggedIn', true);
console.log('用户登录信息已保存');
} catch (e) {
console.error('保存登录信息失败:', e);
}
},
// 获取用户信息
getUserInfo: () => {
try {
const userInfo = uni.getStorageSync('userInfo');
return userInfo ? JSON.parse(userInfo) : null;
} catch (e) {
console.error('获取用户信息失败:', e);
return null;
}
},
// 获取token
getToken: () => {
try {
return uni.getStorageSync('access_token') || '';
} catch (e) {
return '';
}
},
// 检查登录状态
isLoggedIn: () => {
try {
const isLoggedIn = uni.getStorageSync('isLoggedIn');
const token = uni.getStorageSync('access_token');
return !!(isLoggedIn && token);
} catch (e) {
return false;
}
},
// 清除登录信息
clearLoginInfo: () => {
try {
uni.removeStorageSync('access_token');
uni.removeStorageSync('userInfo');
uni.removeStorageSync('isLoggedIn');
console.log('用户登录信息已清除');
} catch (e) {
console.error('清除登录信息失败:', e);
}
},
};
数据缓存规范
// 数据缓存工具 (utils/request.js)
export const storage = {
// 设置缓存
set: (key, value, expire) => {
try {
const data = {
value,
timestamp: Date.now(),
expire: expire ? Date.now() + expire * 1000 : null,
};
uni.setStorageSync(key, JSON.stringify(data));
} catch (e) {
console.error('缓存设置失败:', e);
}
},
// 获取缓存
get: (key) => {
try {
const data = uni.getStorageSync(key);
if (!data) return null;
const parsed = JSON.parse(data);
// 检查是否过期
if (parsed.expire && Date.now() > parsed.expire) {
uni.removeStorageSync(key);
return null;
}
return parsed.value;
} catch (e) {
console.error('缓存获取失败:', e);
return null;
}
},
// 删除缓存
remove: (key) => {
try {
uni.removeStorageSync(key);
} catch (e) {
console.error('缓存删除失败:', e);
}
},
// 清空缓存
clear: () => {
try {
uni.clearStorageSync();
} catch (e) {
console.error('缓存清空失败:', e);
}
},
};
7. 🔐 登录状态管理规范
登录页面开发规范
登录页面模板 (pages/login/index.vue)
<template>
<view class="login-container">
<!-- 背景装饰 -->
<view class="background">
<view class="bg-circles">
<view class="circle circle-1"></view>
<view class="circle circle-2"></view>
<view class="circle circle-3"></view>
</view>
</view>
<!-- 顶部Logo和标题 -->
<view class="header-section">
<view class="logo-container">
<image
class="logo"
src="/static/logo.png"
mode="aspectFit"
@error="handleLogoError"
/>
</view>
<view class="title-container">
<text class="title">智慧校园</text>
<text class="subtitle">杭州第一技师学院</text>
</view>
</view>
<!-- 登录表单 -->
<view class="form-container">
<u-form :model="loginForm" ref="formRef" :rules="rules">
<!-- 租户选择 -->
<u-form-item
v-if="tenantEnabled && tenantList.length > 0"
prop="tenantId"
class="form-item"
>
<u-input
v-model="selectedTenantName"
placeholder="选择租户"
readonly
suffixIcon="arrow-down"
@click="showTenantPicker = true"
:customStyle="inputStyle"
/>
</u-form-item>
<!-- 用户名 -->
<u-form-item prop="username" class="form-item">
<u-input
v-model="loginForm.username"
placeholder="请输入用户名"
prefixIcon="account"
:customStyle="inputStyle"
/>
</u-form-item>
<!-- 密码 -->
<u-form-item prop="password" class="form-item">
<u-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
prefixIcon="lock"
:customStyle="inputStyle"
@confirm="handleLogin"
/>
</u-form-item>
<!-- 登录按钮 -->
<view class="button-container">
<u-button
type="primary"
:loading="loading"
:disabled="loading"
size="large"
shape="round"
:customStyle="buttonStyle"
@click="handleLogin"
>
{{ loading ? '登录中...' : '登录' }}
</u-button>
</view>
</u-form>
</view>
<!-- 租户选择弹窗 -->
<u-picker
:show="showTenantPicker"
:columns="tenantPickerColumns"
@confirm="confirmTenant"
@cancel="showTenantPicker = false"
/>
</view>
</template>
<script>
import { api, userStorage } from '@/utils/request';
export default {
name: 'LoginPage',
data() {
return {
loginForm: {
tenantId: '000000',
username: 'admin',
password: 'admin123',
clientId: 'e5cd7e4891bf95d1d19206ce24a7b32e',
grantType: 'password',
},
rules: {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
},
loading: false,
tenantEnabled: false,
tenantList: [],
selectedTenantName: '',
showTenantPicker: false,
inputStyle: {
height: '45px',
backgroundColor: 'rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
border: '1px solid rgba(255, 255, 255, 0.2)',
color: '#ffffff',
},
buttonStyle: {
height: '50px',
fontSize: '18px',
fontWeight: 'bold',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
borderRadius: '25px',
},
};
},
computed: {
tenantPickerColumns() {
return [
this.tenantList.map((item) => ({
label: item.companyName,
value: item.tenantId,
})),
];
},
},
onLoad() {
this.initTenantList();
},
onShow() {
if (userStorage.isLoggedIn()) {
this.redirectToHome();
}
},
methods: {
async initTenantList() {
try {
const response = await api.getTenantList(false);
if (response && response.data) {
this.tenantEnabled = response.data.tenantEnabled !== false;
if (this.tenantEnabled && response.data.voList) {
this.tenantList = response.data.voList;
if (this.tenantList.length > 0) {
this.loginForm.tenantId = this.tenantList[0].tenantId;
this.selectedTenantName = this.tenantList[0].companyName;
}
}
}
} catch (error) {
console.error('获取租户列表失败:', error);
this.tenantEnabled = false;
}
},
async handleLogin() {
const isValid = await this.$refs.formRef.validate();
if (!isValid) return;
this.loading = true;
try {
const loginResponse = await api.login(this.loginForm);
if (loginResponse && loginResponse.data) {
const userInfoResponse = await api.getUserInfo();
userStorage.saveLoginInfo(loginResponse.data, userInfoResponse.data);
uni.showToast({ title: '登录成功', icon: 'success' });
setTimeout(() => {
this.redirectToHome();
}, 1500);
}
} catch (error) {
console.error('登录失败:', error);
const message = error.msg || error.message || '登录失败,请检查用户名和密码';
uni.showToast({ title: message, icon: 'error', duration: 3000 });
} finally {
this.loading = false;
}
},
confirmTenant(e) {
const selectedIndex = e.indexs[0];
const selectedTenant = this.tenantList[selectedIndex];
this.loginForm.tenantId = selectedTenant.tenantId;
this.selectedTenantName = selectedTenant.companyName;
this.showTenantPicker = false;
},
redirectToHome() {
uni.reLaunch({ url: '/pages/index/index' });
},
handleLogoError() {
console.warn('Logo加载失败,使用默认样式');
},
},
};
</script>
<style lang="scss" scoped>
.login-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 0 30rpx;
}
.background {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
}
.circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-20px);
}
}
.header-section {
flex: 0 0 auto;
text-align: center;
margin-top: 120rpx;
margin-bottom: 80rpx;
z-index: 1;
}
.logo {
width: 120rpx;
height: 120rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
}
.title {
display: block;
font-size: 36rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 16rpx;
}
.form-container {
flex: 1;
z-index: 1;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(20px);
border-radius: 30rpx;
padding: 60rpx 40rpx;
margin: 0 20rpx;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.form-item {
margin-bottom: 40rpx;
}
.button-container {
margin-top: 60rpx;
}
</style>
登录状态验证规范
全局登录验证
每个需要登录的页面都必须在 onLoad 生命周期中检查登录状态:
// 页面登录状态检查模板
export default {
onLoad() {
this.checkLoginStatus();
},
methods: {
checkLoginStatus() {
if (!userStorage.isLoggedIn()) {
uni.reLaunch({
url: '/pages/login/index',
});
return false;
}
return true;
},
loadUserInfo() {
const userInfo = userStorage.getUserInfo();
if (userInfo && userInfo.user) {
this.userInfo.name = userInfo.user.nickName || userInfo.user.userName || '用户';
// 根据需要设置其他用户信息
}
},
},
};
退出登录处理
// 退出登录标准流程
handleLogout() {
uni.showModal({
title: '确认退出',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '正在退出...' });
// 清除登录信息
userStorage.clearLoginInfo();
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '已退出登录',
icon: 'success'
});
// 跳转到登录页面
setTimeout(() => {
uni.reLaunch({
url: '/pages/login/index'
});
}, 500);
}, 1000);
}
},
});
}
应用入口配置
pages.json 登录页配置
{
"pages": [
{
"path": "pages/login/index",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "custom",
"navigationBarBackgroundColor": "transparent",
"backgroundColor": "transparent"
}
},
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom"
}
}
// 其他页面...
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "智慧校园",
"navigationBarBackgroundColor": "rgb(93, 135, 255)",
"backgroundColor": "#f8f9fa"
}
}
登录状态管理最佳实践
1. 安全性原则
// 敏感信息处理
const securityUtils = {
// 敏感数据加密存储
setSecureStorage: (key, value) => {
try {
// 简单的Base64编码,生产环境建议使用更强的加密
const encoded = btoa(JSON.stringify(value));
uni.setStorageSync(key, encoded);
} catch (e) {
console.error('安全存储失败:', e);
}
},
// 敏感数据解密读取
getSecureStorage: (key) => {
try {
const encoded = uni.getStorageSync(key);
if (!encoded) return null;
return JSON.parse(atob(encoded));
} catch (e) {
console.error('安全读取失败:', e);
return null;
}
},
};
2. 自动登录检查
// App.vue 中的全局登录状态检查
export default {
onLaunch() {
this.checkAutoLogin();
},
methods: {
checkAutoLogin() {
const isLoggedIn = userStorage.isLoggedIn();
const currentPages = getCurrentPages();
const currentRoute = currentPages[currentPages.length - 1]?.route;
// 如果未登录且不在登录页,跳转到登录页
if (!isLoggedIn && currentRoute !== 'pages/login/index') {
uni.reLaunch({
url: '/pages/login/index',
});
}
},
},
};
3. Token 过期处理
// 请求拦截器中的Token过期处理
export const request = (options) => {
return new Promise((resolve, reject) => {
uni.request({
// 请求配置...
success: (res) => {
if (res.statusCode === 401) {
// Token过期处理
userStorage.clearLoginInfo();
uni.showToast({
title: '登录已过期,请重新登录',
icon: 'none',
});
setTimeout(() => {
uni.reLaunch({
url: '/pages/login/index',
});
}, 2000);
return;
}
// 其他响应处理...
},
});
});
};
8. 📱 页面配置规范
pages.json 配置
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom"
}
},
{
"path": "pages/workbench/index",
"style": {
"navigationBarTitleText": "办公中心",
"navigationStyle": "custom"
}
},
{
"path": "pages/profile/index",
"style": {
"navigationBarTitleText": "我的",
"navigationStyle": "custom"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "智慧校园",
"navigationBarBackgroundColor": "rgb(93, 135, 255)",
"backgroundColor": "#f8f9fa"
},
"tabBar": {
"color": "#999999",
"selectedColor": "rgb(93, 135, 255)",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"height": "60px",
"fontSize": "12px",
"iconWidth": "22px",
"iconHeight": "22px",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/tabbar/home.svg",
"selectedIconPath": "static/tabbar/home-active.svg"
},
{
"pagePath": "pages/workbench/index",
"text": "办公中心",
"iconPath": "static/tabbar/workbench.svg",
"selectedIconPath": "static/tabbar/workbench-active.svg"
},
{
"pagePath": "pages/profile/index",
"text": "我的",
"iconPath": "static/tabbar/profile.svg",
"selectedIconPath": "static/tabbar/profile-active.svg"
}
]
}
}
manifest.json 配置
{
"name": "智慧校园",
"appid": "",
"description": "基于Vue3+uniapp的智慧校园移动应用",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"vueVersion": "3",
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>"
]
},
"ios": {},
"sdkConfigs": {}
}
},
"quickapp": {},
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
}
}
9. 🎭 用户体验规范
交互设计规范
动画效果使用
// utils/animations.js - 动画工具
export const animationUtils = {
// 淡入动画
fadeIn: (element) => {
element.style.animation = 'fadeIn 0.3s ease';
},
// 滑入动画
slideUp: (element) => {
element.style.animation = 'slideUp 0.3s ease';
},
// 缩放动画
scale: (element) => {
element.style.animation = 'scale 0.2s ease';
},
}
// 在页面中使用
methods: {
handleItemClick() {
// 触觉反馈
uni.vibrateShort();
// 视觉反馈
// 处理点击逻辑
}
}
手势操作支持
// utils/gestures.js - 手势识别
export const gestureManager = {
// 初始化手势
init: (element, callbacks) => {
let startX, startY, startTime;
element.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
startTime = Date.now();
});
element.addEventListener('touchend', (e) => {
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const endTime = Date.now();
// 判断手势类型
const deltaX = endX - startX;
const deltaY = endY - startY;
const deltaTime = endTime - startTime;
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 50) {
if (deltaX > 0) {
callbacks.swipeRight && callbacks.swipeRight();
} else {
callbacks.swipeLeft && callbacks.swipeLeft();
}
}
});
},
};
无障碍访问规范
// utils/accessibility.js - 无障碍访问工具
export const accessibilityUtils = {
// 设置焦点
setFocus: (element) => {
element.focus();
},
// 添加ARIA标签
addAriaLabel: (element, label) => {
element.setAttribute('aria-label', label);
},
// 键盘导航支持
enableKeyboardNavigation: (container) => {
container.addEventListener('keydown', (e) => {
switch (e.key) {
case 'Tab':
// Tab导航逻辑
break;
case 'Enter':
case ' ':
// 激活元素
e.target.click();
break;
case 'Escape':
// 取消操作
break;
}
});
},
};
10. 🚀 性能优化规范
代码优化
组件懒加载
// 页面中使用懒加载
export default {
components: {
// 懒加载组件
LazyComponent: () => import('@/components/LazyComponent.vue'),
},
};
图片优化
<template>
<!-- 响应式图片 -->
<image :src="imageSrc" mode="aspectFit" lazy-load :fade-show="true" @error="handleImageError" />
</template>
<script>
export default {
methods: {
handleImageError() {
// 图片加载失败处理
console.log('图片加载失败');
},
},
};
</script>
网络优化
请求缓存策略
// 智能缓存策略
export const cacheStrategy = {
// 短期缓存 (5分钟)
short: 5 * 60,
// 中期缓存 (30分钟)
medium: 30 * 60,
// 长期缓存 (24小时)
long: 24 * 60 * 60,
};
// API调用示例
export const api = {
getUserInfo: () => {
const cacheKey = 'userInfo';
const cached = storage.get(cacheKey);
if (cached) {
return Promise.resolve(cached);
}
return request({
url: '/user/info',
}).then((data) => {
storage.set(cacheKey, data, cacheStrategy.medium);
return data;
});
},
};
11. 🛠️ 开发工具配置
Vite 配置 (vite.config.js)
import { defineConfig } from 'vite';
import uni from '@dcloudio/vite-plugin-uni';
export default defineConfig({
plugins: [uni()],
server: {
port: 3000,
open: true,
},
build: {
// 生产环境优化
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
},
});
ESLint 配置 (eslint.config.cjs)
module.exports = [
{
files: ['src/**/*.{js,vue}'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
rules: {
indent: ['error', 2],
quotes: ['error', 'single'],
semi: ['error', 'never'],
'no-console': 'warn',
'no-debugger': 'error',
},
},
];
12. 📋 开发命令规范
常用命令
# 安装依赖
npm install
# H5开发
npm run dev:h5
# 微信小程序开发
npm run dev:mp-weixin
# App开发
npm run dev:app
# 构建H5
npm run build:h5
# 构建微信小程序
npm run build:mp-weixin
# 代码检查
npm run lint
# 代码自动修复
npm run lint:fix
13. ✅ 最佳实践总结
代码规范
- 文件组织: 按功能模块组织文件结构
- 组件复用: 提取通用组件,避免重复代码
- 样式管理: 使用 CSS 变量系统,便于主题切换
- 性能优化: 合理使用缓存和懒加载
- 用户体验: 添加加载状态、错误处理和无障碍支持
开发流程
- 需求分析 → 确定页面结构和功能
- 组件设计 → 拆分可复用组件
- 页面开发 → 按模板编写页面代码
- 样式调优 → 使用统一的设计规范
- 功能测试 → 多平台测试确保兼容性
- 性能优化 → 优化加载速度和用户体验
质量保证
- 代码审查: 确保代码符合规范
- 功能测试: 确保所有功能正常工作
- 兼容性测试: 确保多平台兼容
- 性能测试: 确保应用性能满足要求
- 用户体验测试: 确保交互流畅自然
🎯 总结
本规范基于实际的 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
uview-plus 图标组件规范(2025-07-01 新增)
- 移动端UI开发中,图标组件统一使用 (uview-plus 3.x 官方标准),禁止使用 或 。
- 推荐通过 easycom 自动全局注册,无需手动 import。
- 标准用法示例:
<u-icon name="account" size="22" color="#b0b8c9" /> <u-icon name="lock-open" size="22" color="#b0b8c9" /> - 图标名称请参考 uview-plus 官方文档:https://v3.uviewui.com/components/icon.html
uview-plus 图标字体配置规范(2025-08-06 更新)
📦 版本升级经验总结
升级记录: uview-plus 3.4.45 → 3.4.73 (+28个版本) 升级时间: 2025-08-06 配置优化: 采用官方推荐的字体加载机制
🔧 官方推荐配置方案
方案1: 自托管字体配置(官方推荐 ⭐)
参考文档: https://uview-plus.jiangruyi.com/components/icon.html#自定义默认字体图标自托管资源
// main.js - 官方推荐配置方式
app.use(uviewPlus, () => {
return {
options: {
config: {
// 🔧 自托管字体图标资源地址,避免CDN依赖问题
iconUrl: '/static/fonts/uview-plus-official.ttf'
}
}
}
});
方案2: 手动加载字体(官方兼容性方案)
参考文档: https://uview-plus.jiangruyi.com/components/icon.html#新版本全局加载兼容性
// App.vue - 如果在小程序等场景出现图标无法全局加载
import { fontUtil } from 'uview-plus';
export default {
onLaunch: function () {
// 官方推荐:手动加载uview-plus字体图标
fontUtil.loadFont();
}
};
方案3: CSS备用方案(平台兼容)
/* App.vue - 其他平台如抖音/QQ/百度小程序的备用方案 */
@font-face {
font-family: 'uicon-iconfont';
src: url('/static/fonts/uview-plus-official.ttf') format('truetype');
}
🏆 最佳实践配置
// main.js - 完整配置示例
import { createSSRApp } from 'vue';
import App from './App.vue';
import uviewPlus from 'uview-plus';
import 'uview-plus/index.scss';
import '@/static/fonts/iconfont.css'; // 自定义SVG图标库
export function createApp() {
const app = createSSRApp(App);
// 🔧 官方推荐:uview-plus配置
app.use(uviewPlus, () => {
return {
options: {
config: {
iconUrl: '/static/fonts/uview-plus-official.ttf'
}
}
}
});
return { app };
}
// App.vue - 兼容性配置
import { fontUtil } from 'uview-plus';
export default {
onLaunch: function () {
// 🔧 官方推荐:手动加载字体图标
try {
fontUtil.loadFont();
console.log('✅ 手动加载uview-plus字体成功');
} catch (error) {
console.error('❌ 手动加载uview-plus字体失败:', error);
}
}
};
混合图标使用策略
- uview-plus图标: 使用
<u-icon name="home" />系统通用图标 - 项目iconfont: 使用
<text class="iconfont icon-user"></text>业务专用图标 - 优先级: 项目iconfont > uview-plus图标 > CDN字体
⚠️ 重要注意事项
升级前检查
- 字体文件: 确保本地字体文件存在且可访问
- 网络环境: 测试CDN地址访问性
- 版本兼容: 检查字体版本与组件版本匹配
最佳实践
- 优先级: 项目iconfont > 本地字体 > CDN字体
- 测试覆盖: 在不同网络环境下测试图标显示
- 回退机制: 准备备用图标方案
- 文档记录: 及时更新字体配置变更
📚 相关文件位置
- 字体配置:
mobile/src/main.js - 测试页面:
mobile/src/pages/demo/uview-icon-test/index.vue - 字体文件:
mobile/src/static/fonts/ - 项目图标:
mobile/src/static/fonts/iconfont.css
🧪 测试验证
# 启动开发服务器
npm run dev:h5
# 访问测试页面
http://localhost:3000/#/pages/demo/uview-icon-test/index
📖 参考文档
- uview-plus官方文档: https://uview-plus.jiangruyi.com/
- 字体图标组件: https://uview-plus.jiangruyi.com/components/icon.html
- 自托管配置: https://uview-plus.jiangruyi.com/components/icon.html#自定义默认字体图标自托管资源
- 兼容性配置: https://uview-plus.jiangruyi.com/components/icon.html#新版本全局加载兼容性
- 项目图标文档:
docs/开发文档/API/u-icon替换映射表.md