mobile/ (智慧校园移动应用){
"name": "mobile2",
"version": "0.1.0",
"dependencies": {
"@dcloudio/uni-app": "^2.0.2-4070520250711001",
"uview-ui": "2.0.38",
"vue": ">= 2.6.14 < 2.7",
"vue-class-component": "^6.3.2",
"vue-property-decorator": "^8.0.0",
"vuex": "^3.2.0"
},
"devDependencies": {
"sass": "^1.90.0",
"vue-template-compiler": ">= 2.6.14 < 2.7",
"typescript": "~4.5.5"
}
}
mobile/
├── src/ # 源码目录
│ ├── components/ # 通用组件
│ │ ├── Card.vue # 卡片组件
│ │ ├── ServiceItem.vue # 服务项组件
│ │ └── GridLayout.vue # 网格布局组件
│ ├── pages/ # 页面文件
│ │ ├── index/ # 首页
│ │ │ └── index.vue # 首页内容
│ │ ├── login/ # 登录页
│ │ │ └── index.vue # 登录页面
│ │ └── task/ # 工作流页面
│ │ ├── myDocument.vue # 我发起的
│ │ ├── allTaskWaiting.vue # 我的待办
│ │ ├── taskFinish.vue # 我的已办
│ │ └── taskCopyList.vue # 抄送我的
│ ├── utils/ # 工具类
│ │ ├── request.js # 网络请求工具
│ │ ├── user.js # 用户管理工具
│ │ ├── permission.js # 权限管理工具
│ │ └── router.js # 路由管理工具
│ ├── common/ # 公共模块
│ │ ├── request.js # API请求封装
│ │ ├── storage.js # 存储工具
│ │ └── crypto.js # 加密工具
│ ├── static/ # 静态资源
│ │ ├── fonts/ # 字体文件
│ │ │ └── iconfont.css # 自定义图标
│ │ └── images/ # 图片资源
│ ├── App.vue # 应用入口
│ ├── main.ts # 项目入口
│ ├── pages.json # 页面配置
│ ├── manifest.json # 应用配置
│ └── uni.scss # 全局样式
├── package.json # 项目配置
├── vue.config.js # Vue CLI配置
└── README.md # 项目说明
<template>
<view class="page-container">
<!-- 自定义导航栏 -->
<view class="custom-navbar">
<text class="navbar-title">{{ pageTitle }}</text>
</view>
<!-- 主要内容区域 -->
<scroll-view class="main-content" scroll-y enhanced :show-scrollbar="false">
<!-- 页面内容 -->
<view class="content">
<!-- 使用uView组件 -->
<u-cell-group>
<u-cell title="示例项目"></u-cell>
</u-cell-group>
</view>
<!-- 底部间距 -->
<view class="bottom-spacer"></view>
</scroll-view>
</view>
</template>
<script>
export default {
name: 'PageName',
data() {
return {
pageTitle: '页面标题',
// 页面数据
};
},
onLoad() {
// 页面加载逻辑
this.initPage();
},
onShow() {
// 页面显示逻辑
},
onUnload() {
// 页面卸载时清理定时器等
if (this.timer) {
clearInterval(this.timer);
}
},
methods: {
initPage() {
// 初始化页面逻辑
},
handleRefresh() {
// 刷新逻辑
uni.showLoading({ title: '刷新中...' });
setTimeout(() => {
uni.hideLoading();
uni.showToast({ title: '刷新成功', icon: 'success' });
}, 1000);
},
},
};
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: var(--background-color);
display: flex;
flex-direction: column;
}
.custom-navbar {
background: linear-gradient(135deg, #007aff, #0051d9);
padding: calc(var(--status-bar-height) + 12px) 16px 16px;
display: flex;
align-items: center;
justify-content: center;
}
.navbar-title {
font-size: 18px;
font-weight: 600;
color: #ffffff;
}
.main-content {
flex: 1;
padding-bottom: calc(50px + env(safe-area-inset-bottom));
}
.content {
padding: var(--spacing-md);
}
.bottom-spacer {
height: 20px;
}
</style>
// 主题色彩变量
: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;
}
<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>
Card.vue, ServiceItem.vue)Card, ServiceItem)<Card> 或 <card>)⚠️ 重要说明: 移动端项目使用 iconfont 图标系统,同时支持 uview-ui 的 u-icon 组件!
✅ 优先使用 iconfont 图标
<!-- ✅ 推荐:使用 iconfont 图标 -->
<text class="iconfont icon-search" style="font-size: 20px; color: #666;"></text>
<text class="iconfont icon-account-circle" style="font-size: 24px;"></text>
<text class="iconfont icon-close" @click="handleClose"></text>
vue
<!-- ✅ 支持:使用 uview-ui 图标 -->
<u-icon name="search" size="20" color="#666"></u-icon>
<u-icon name="account" size="24"></u-icon>
<u-icon name="close" @click="handleClose"></u-icon>
🔄 动态图标类名
<!-- ✅ 动态切换图标 -->
<text
:class="'iconfont ' + (expanded ? 'icon-arrow-down' : 'icon-arrow-right')"
style="font-size: 16px; color: #999;"
@click="toggleExpand"
></text>
位置: mobile/src/static/fonts/iconfont.css
.iconfont {
display: inline-block;
width: 16px;
height: 16px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
/* 用户图标 */
.icon-user {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z'/%3E%3C/svg%3E");
}
/* 密码锁图标 */
.icon-lock {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6z'/%3E%3C/svg%3E");
}
/* 更多图标... */
位置: mobile/src/main.ts
import Vue from 'vue'
import App from './App.vue'
import './uni.promisify.adaptor'
import uView from "uview-ui";
Vue.use(uView);
Vue.config.productionTip = false
const app = new (typeof App === 'function' ? App : Vue.extend(Object.assign({ mpType: 'app' }, App)))
app.$mount();
位置: mobile/src/App.vue
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
mpType: 'app',
onLaunch() {
console.log('App Launch');
},
onShow() {
console.log('App Show');
},
onHide() {
console.log('App Hide');
},
});
</script>
<style lang="scss">
/* 注意要写在第一行,同时给style标签加入lang="scss"属性 */
@import 'uview-ui/index.scss';
/* 引入自定义图标系统 */
@import '@/static/fonts/iconfont.css';
</style>
<template>
<!-- iconfont 图标 -->
<text class="iconfont icon-user"></text>
<text class="iconfont icon-lock"></text>
<text class="iconfont icon-search"></text>
<!-- uview-ui 图标 -->
<u-icon name="account" size="20" color="#666"></u-icon>
<u-icon name="lock" size="20" color="#666"></u-icon>
<u-icon name="search" size="20" color="#666"></u-icon>
<!-- 在表单中使用 -->
<view class="input-group">
<text class="iconfont icon-user input-icon"></text>
<input class="login-input" placeholder="账号" />
</view>
<!-- 在按钮中使用 -->
<button class="btn-primary">
<text class="iconfont icon-login btn-icon"></text>
<text>登录</text>
</button>
</template>
移动端列表中的操作按钮统一采用图标+文字垂直排列的无边框设计,提升用户体验和界面美观度。
<!-- 标准操作按钮容器 -->
<view class="card-actions">
<!-- 查看详细按钮 -->
<view class="action-btn info-btn" @click="handleView(item)">
<u-icon name="eye" size="16" color="#3c9cff"></u-icon>
<text class="btn-text">查看详细</text>
</view>
<!-- 编辑修改按钮 -->
<view class="action-btn success-btn" @click="handleEdit(item)">
<u-icon name="edit-pen" size="16" color="#5ac725"></u-icon>
<text class="btn-text">编辑</text>
</view>
<!-- 历史记录按钮 -->
<view class="action-btn warning-btn" @click="handleHistory(item)">
<u-icon name="clock" size="16" color="#ff9900"></u-icon>
<text class="btn-text">历史记录</text>
</view>
<!-- 删除操作按钮 -->
<view class="action-btn error-btn" @click="handleDelete(item)">
<u-icon name="trash" size="16" color="#f56c6c"></u-icon>
<text class="btn-text">删除</text>
</view>
</view>
/* 操作按钮容器 */
.card-actions {
display: flex;
justify-content: space-around;
padding: 20rpx;
border-top: 1px solid #f0f0f0;
gap: 16rpx;
}
/* 统一按钮样式 */
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16rpx 20rpx;
border-radius: 12rpx;
background-color: #f8f9fa;
min-width: 120rpx;
transition: all 0.3s ease;
/* 点击动画效果 */
&:active {
transform: scale(0.95);
opacity: 0.8;
}
}
/* 按钮文字样式 */
.btn-text {
font-size: 24rpx;
margin-top: 8rpx;
color: #303133;
font-weight: 500;
}
/* 主题色彩方案 */
.info-btn {
background-color: rgba(60, 156, 255, 0.1);
.btn-text { color: #3c9cff; }
}
.success-btn {
background-color: rgba(90, 199, 37, 0.1);
.btn-text { color: #5ac725; }
}
.warning-btn {
background-color: rgba(255, 153, 0, 0.1);
.btn-text { color: #ff9900; }
}
.error-btn {
background-color: rgba(245, 108, 108, 0.1);
.btn-text { color: #f56c6c; }
}
.primary-btn {
background-color: rgba(51, 102, 255, 0.1);
.btn-text { color: #3366ff; }
}
.default-btn {
background-color: rgba(144, 147, 153, 0.1);
.btn-text { color: #909399; }
}
#### 🎨 按钮背景色规范
**📱 移动端按钮背景色设计规范**
**背景色选项**:
- ✅ **完全透明**: `rgba(r, g, b, 0)` - 无背景色,纯文字按钮
- ✅ **半透明背景色**: `rgba(r, g, b, 0.1)` 到 `rgba(r, g, b, 0.3)` - 有背景色的按钮
- ✅ **实色背景**: `rgba(r, g, b, 1)` - 完全实色背景
**推荐使用场景**:
- **完全透明背景**: 适用于简洁设计,强调文字内容
- **半透明背景**: 适用于需要视觉层次的场景
- **实色背景**: 适用于重要操作按钮
**当前项目使用**:
```scss
.info-btn {
background-color: rgba(60, 156, 255, 0); // ✅ 完全透明
.btn-text { color: #3c9cff; }
}
.success-btn {
background-color: rgba(90, 199, 37, 0); // ✅ 完全透明
.btn-text { color: #5ac725; }
}
.warning-btn {
background-color: rgba(255, 153, 0, 0); // ✅ 完全透明
.btn-text { color: #ff9900; }
}
设计原则:
符合项目设计风格
#### 🔧 图标选择规范
| 功能类型 | 推荐图标 | 颜色 | 样式类 | 使用场景 |
|---------|---------|------|--------|----------|
| 查看详细 | `eye` | #3c9cff | info-btn | 查看、预览、详情 |
| 编辑修改 | `edit-pen` | #5ac725 | success-btn | 编辑、修改、更新 |
| 历史记录 | `clock` | #ff9900 | warning-btn | 时间、历史、记录 |
| 删除操作 | `trash` | #f56c6c | error-btn | 删除、移除、清空 |
| 下载导出 | `download` | #3c9cff | info-btn | 下载、导出、保存 |
| 上传提交 | `upload` | #5ac725 | success-btn | 上传、提交、发送 |
| 审核通过 | `checkmark` | #5ac725 | success-btn | 通过、确认、同意 |
| 审核拒绝 | `close` | #f56c6c | error-btn | 拒绝、关闭、取消 |
| 详情信息 | `info-circle` | #3c9cff | info-btn | 信息、说明、帮助 |
| 设置配置 | `setting` | #909399 | default-btn | 设置、配置、管理 |
| 分享转发 | `share` | #3366ff | primary-btn | 分享、转发、推送 |
| 收藏点赞 | `heart` | #f56c6c | error-btn | 收藏、点赞、喜欢 |
#### 💡 使用指导
1. **✅ 强制使用场景**
- 所有移动端列表页面的操作按钮
- 卡片组件中的操作按钮
- 表格行操作按钮
2. **🎨 设计要求**
- 图标大小统一为16px
- 文字大小统一为24rpx(12px)
- 按钮最小宽度120rpx
- 圆角半径12rpx
3. **🔄 交互要求**
- 必须包含点击动画效果(:active伪类)
- 缩放比例0.95,透明度0.8
- 过渡动画时间0.3s
4. **📏 布局要求**
- 使用flex布局,space-around分布
- 按钮间距16rpx
- 容器内边距20rpx
- 顶部边框分割线
#### 🚨 注意事项
- **禁止使用传统边框按钮**: 移动端列表不再使用`<u-button>`组件
- **图标组件规范**: 统一使用`<u-icon />`组件
- **颜色主题一致**: 严格按照表格中的颜色方案
- **响应式适配**: 确保在不同屏幕尺寸下正常显示
- **无障碍访问**: 文字描述清晰,图标语义明确
#### 📝 完整示例
```vue
<template>
<view class="list-container">
<!-- 列表项 -->
<view class="list-item" v-for="item in dataList" :key="item.id">
<!-- 主要内容 -->
<view class="item-content">
<text class="item-title">{{ item.title }}</text>
<text class="item-desc">{{ item.description }}</text>
</view>
<!-- 操作按钮 -->
<view class="card-actions">
<view class="action-btn info-btn" @click="handleView(item)">
<u-icon name="eye" size="16" color="#3c9cff"></u-icon>
<text class="btn-text">查看详细</text>
</view>
<view v-if="item.canEdit" class="action-btn success-btn" @click="handleEdit(item)">
<u-icon name="edit-pen" size="16" color="#5ac725"></u-icon>
<text class="btn-text">编辑</text>
</view>
<view class="action-btn warning-btn" @click="handleHistory(item)">
<u-icon name="clock" size="16" color="#ff9900"></u-icon>
<text class="btn-text">历史记录</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
dataList: [
{ id: 1, title: '示例标题1', description: '示例描述', canEdit: true },
{ id: 2, title: '示例标题2', description: '示例描述', canEdit: false }
]
}
},
methods: {
handleView(item) {
console.log('查看详细:', item);
// 跳转到详情页面
},
handleEdit(item) {
console.log('编辑项目:', item);
// 跳转到编辑页面
},
handleHistory(item) {
console.log('查看历史:', item);
// 跳转到历史页面
}
}
}
</script>
<style lang="scss" scoped>
.list-container {
padding: 20rpx;
}
.list-item {
background-color: #ffffff;
border-radius: 12rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.item-content {
padding: 30rpx 30rpx 20rpx;
}
.item-title {
font-size: 32rpx;
font-weight: 600;
color: #303133;
margin-bottom: 10rpx;
}
.item-desc {
font-size: 28rpx;
color: #606266;
}
/* 引入标准按钮样式 */
.card-actions {
display: flex;
justify-content: space-around;
padding: 20rpx;
border-top: 1px solid #f0f0f0;
gap: 16rpx;
}
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16rpx 20rpx;
border-radius: 12rpx;
background-color: #f8f9fa;
min-width: 120rpx;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
opacity: 0.8;
}
}
.btn-text {
font-size: 24rpx;
margin-top: 8rpx;
color: #303133;
font-weight: 500;
}
.info-btn {
background-color: rgba(60, 156, 255, 0.1);
.btn-text { color: #3c9cff; }
}
.success-btn {
background-color: rgba(90, 199, 37, 0.1);
.btn-text { color: #5ac725; }
}
.warning-btn {
background-color: rgba(255, 153, 0, 0.1);
.btn-text { color: #ff9900; }
}
</style>
// API请求配置
const baseURL =
process.env.NODE_ENV === 'development'
? 'http://localhost:8080'
: 'https://your-api-domain.com';
// 统一请求方法
export const request = (options) => {
return new Promise((resolve, reject) => {
uni.request({
url: baseURL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: {
'Content-Type': 'application/json',
Authorization: getToken(),
...options.header,
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data);
} else {
reject(res);
}
},
fail: (error) => {
reject(error);
},
});
});
};
// 获取token
function getToken() {
try {
const token = uni.getStorageSync('token');
return token ? `Bearer ${token}` : '';
} catch (e) {
return '';
}
}
// API方法封装
export const api = {
// 用户登录
login: (data) =>
request({
url: '/auth/login',
method: 'POST',
data: {
tenantId: data.tenantId || '000000',
username: data.username,
password: data.password,
clientId: data.clientId || 'e5cd7e4891bf95d1d19206ce24a7b32e',
grantType: data.grantType || 'password',
},
header: {
isToken: false,
isEncrypt: true,
repeatSubmit: false,
},
}),
// 获取用户信息
getUserInfo: () =>
request({
url: '/system/user/getInfo',
method: 'GET',
}),
// 获取租户列表
getTenantList: (isToken = false) =>
request({
url: '/auth/tenant/list',
method: 'GET',
header: {
isToken: isToken,
},
}),
// 注销登录
logout: () =>
request({
url: '/auth/logout',
method: 'POST',
}),
};
// 用户信息缓存工具
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);
}
},
};
<template>
<view class="login-bg">
<view class="login-card">
<!-- Logo和标题区域 -->
<view class="login-header">
<view class="login-logo">
<image class="logo-img" src="/static/logo-2.png" mode="aspectFit" />
</view>
<view class="login-text">
<view class="login-title">智慧校园</view>
<view class="login-subtitle">杭州第一技师学院</view>
</view>
</view>
<!-- 登录表单 -->
<view class="login-form">
<!-- 账号输入框 -->
<view class="input-wrapper">
<u-input
v-model="username"
placeholder="教工号/学号"
type="text"
border="true"
:custom-style="inputStyle"
placeholder-style="color: #999; font-size: 16px"
>
<template #prefix>
<u-icon
name="account"
size="20"
color="#b0b8c9"
class="input-prefix-icon"
/>
</template>
</u-input>
</view>
<!-- 密码输入框 -->
<view class="input-wrapper">
<u-input
v-model="password"
placeholder="密码"
type="password"
border="true"
:custom-style="inputStyle"
placeholder-style="color: #999; font-size: 16px"
@confirm="onPasswordConfirm"
>
<template #prefix>
<u-icon
name="lock"
size="20"
color="#b0b8c9"
class="input-prefix-icon"
/>
</template>
</u-input>
</view>
<!-- 登录按钮 -->
<u-button
type="primary"
:custom-style="buttonStyle"
:loading="loading"
:disabled="loading"
@click="onLogin"
>
<view class="login-btn-content" v-if="!loading">
<text class="login-btn-text">登录</text>
</view>
<view class="login-btn-content" v-else>
<text class="login-btn-text">登录中...</text>
</view>
</u-button>
</view>
<!-- 版权信息 -->
<view class="login-copyright">© 2025 杭州第一技师学院 All Rights Reserved.</view>
</view>
</view>
</template>
<script>
import { api } from '@/common/request';
import { userStorage } from '@/common/storage';
export default {
name: 'LoginPage',
data() {
return {
username: '',
password: '',
loading: false,
inputStyle: {
height: '45px',
backgroundColor: 'rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
border: '1px solid rgba(255, 255, 255, 0.2)',
color: '#ffffff',
},
buttonStyle: {
height: '50px',
fontSize: '18px',
fontWeight: 'bold',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
borderRadius: '25px',
},
};
},
onLoad() {
// 检查是否已登录
if (userStorage.isLoggedIn()) {
this.redirectToHome();
}
},
methods: {
async onLogin() {
if (!this.username || !this.password) {
uni.showToast({
title: '请输入用户名和密码',
icon: 'none',
});
return;
}
this.loading = true;
try {
const loginResponse = await api.login({
username: this.username,
password: this.password,
});
if (loginResponse && loginResponse.data) {
const userInfoResponse = await api.getUserInfo();
userStorage.saveLoginInfo(loginResponse.data, userInfoResponse.data);
uni.showToast({ title: '登录成功', icon: 'success' });
setTimeout(() => {
this.redirectToHome();
}, 1500);
}
} catch (error) {
console.error('登录失败:', error);
const message = error.msg || error.message || '登录失败,请检查用户名和密码';
uni.showToast({ title: message, icon: 'error', duration: 3000 });
} finally {
this.loading = false;
}
},
onPasswordConfirm() {
this.onLogin();
},
redirectToHome() {
uni.reLaunch({ url: '/pages/index/index' });
},
},
};
</script>
<style lang="scss" scoped>
.login-bg {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
border-radius: 20px;
padding: 40px 30px;
width: 100%;
max-width: 400px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.login-header {
text-align: center;
margin-bottom: 40px;
}
.login-logo {
margin-bottom: 20px;
}
.logo-img {
width: 80px;
height: 80px;
border-radius: 15px;
}
.login-title {
font-size: 28px;
font-weight: bold;
color: #ffffff;
margin-bottom: 8px;
}
.login-subtitle {
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
}
.login-form {
margin-bottom: 30px;
}
.input-wrapper {
margin-bottom: 20px;
}
.login-btn-content {
display: flex;
align-items: center;
justify-content: center;
}
.login-btn-text {
color: #ffffff;
font-size: 18px;
font-weight: bold;
}
.login-copyright {
text-align: center;
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
margin-top: 20px;
}
</style>
每个需要登录的页面都必须在 onLoad 生命周期中检查登录状态:
// 页面登录状态检查模板
export default {
onLoad() {
this.checkLoginStatus();
},
methods: {
checkLoginStatus() {
if (!userStorage.isLoggedIn()) {
uni.reLaunch({
url: '/pages/login/index',
});
return false;
}
return true;
},
loadUserInfo() {
const userInfo = userStorage.getUserInfo();
if (userInfo && userInfo.user) {
this.userInfo.name = userInfo.user.nickName || userInfo.user.userName || '用户';
// 根据需要设置其他用户信息
}
},
},
};
{
"easycom": {
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
},
"pages": [
{
"path": "pages/login/index",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "杭州第一技师学院智慧校园",
"navigationStyle": "custom"
}
},
{
"path": "pages/task/myDocument",
"style": {
"navigationBarTitleText": "我发起的",
"navigationBarBackgroundColor": "#5d87ff",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/task/allTaskWaiting",
"style": {
"navigationBarTitleText": "我的待办",
"navigationBarBackgroundColor": "#5d87ff",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/task/taskFinish",
"style": {
"navigationBarTitleText": "我的已办",
"navigationBarBackgroundColor": "#5d87ff",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/task/taskCopyList",
"style": {
"navigationBarTitleText": "抄送我的",
"navigationBarBackgroundColor": "#5d87ff",
"navigationBarTextStyle": "white"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "智慧校园",
"navigationBarBackgroundColor": "#5d87ff",
"backgroundColor": "#F8F8F8"
}
}
{
"name": "mobile2",
"appid": "",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>"
]
},
"ios": {},
"sdkConfigs": {}
},
"usingComponents": true
},
"quickapp": {},
"mp-weixin": {
"usingComponents": true,
"appid": "",
"setting": {
"urlCheck": true
}
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"mp-qq": {
"usingComponents": true
}
}
// 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;
}
});
},
};
// 页面中使用懒加载
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;
});
},
};
⚠️ 重要: uni-app 项目中,开发服务器配置应统一在 manifest.json 的 h5.devServer 中:
{
"h5": {
"devServer": {
"https": false,
"allowedHosts": "all",
"port": 8081,
"host": "0.0.0.0",
"proxy": {
"/api": {
"target": "http://localhost:8080",
"changeOrigin": true,
"pathRewrite": {
"^/api": ""
}
}
}
},
"publicPath": "./",
"router": {
"mode": "hash"
},
"title": "智慧校园移动端"
}
}
🔧 仅用于构建优化,避免与 manifest.json 产生配置冲突:
module.exports = {
transpileDependencies: ['uview-ui'],
configureWebpack: {
resolve: {
alias: {
'@': require('path').resolve(__dirname, 'src'),
},
},
},
};
uni-app 配置优先级:
manifest.json > vue.config.jsmanifest.jsonmanifest.json最佳实践:
✅ 统一配置: 开发服务器配置统一在 manifest.json 中
✅ 构建工具: vue.config.js 仅用于构建优化
✅ 避免冲突: 不要在两个文件中重复配置同一项目
✅ 多端兼容: 确保配置在所有平台都生效
# 安装依赖
npm install
# H5开发
npm run dev:h5
# 微信小程序开发
npm run dev:mp-weixin
# App开发
npm run dev:app-plus
# 构建H5
npm run build:h5
# 构建微信小程序
npm run build:mp-weixin
# 构建App
npm run build:app-plus
本规范基于实际的 Vue 2 + uniapp + uview-ui 项目制定,提供了完整的移动端开发流程。
✨ 技术特色:
🎨 设计理念:
📱 平台支持:
遵循本规范能够确保代码质量、提升开发效率,并为用户提供优秀的移动端体验。
标准用法示例:
<u-icon name="account" size="22" color="#b0b8c9" />
<u-icon name="lock-open" size="22" color="#b0b8c9" />
⚠️ 重要:u-picker组件必须严格按照以下规范使用,否则会出现数据显示空白问题!
<!-- ✅ 正确用法 -->
<u-picker
:columns="[optionsArray]" <!-- 必须是二维数组格式 -->
keyName="label" <!-- 指定显示字段名 -->
v-model="indexArray" <!-- 选中项索引数组 -->
:show="showPicker" <!-- 控制显示/隐藏 -->
@confirm="onConfirm" <!-- 确认事件 -->
@close="showPicker = false" <!-- 关闭事件 -->
></u-picker>
<!-- ❌ 错误用法(旧版本写法)-->
<u-picker
:range="optionsArray" <!-- 错误:应该用columns -->
range-key="label" <!-- 错误:应该用keyName -->
@change="onChange" <!-- 错误:应该用confirm -->
></u-picker>
// ✅ 正确的数据格式
data() {
return {
// 对象数组数据
optionsArray: [
{ label: "选项1", value: "1" },
{ label: "选项2", value: "2" },
{ label: "选项3", value: "3" }
],
// 选中索引(必须是数组格式)
indexArray: [0],
// 控制显示
showPicker: false
}
}
// 在模板中使用时,columns必须包装成二维数组
:columns="[optionsArray]" // 注意中括号包装
// ✅ 正确的事件处理
onConfirm(e) {
console.log('picker选择结果:', e);
const index = e.value[0]; // uview2格式:e.value
const selectedItem = this.optionsArray[index];
this.form.fieldName = selectedItem.value;
this.indexArray = [index];
this.showPicker = false;
}
// ❌ 错误的事件处理
onConfirm(e) {
const index = e.detail.value[0]; // 错误:小程序原生格式
}
<template>
<view>
<!-- 显示区域 -->
<view class="picker-input" @click="showPicker = true">
{{ getSelectedLabel() || '请选择' }}
</view>
<!-- picker组件 -->
<u-picker
:columns="[optionsArray]"
keyName="label"
v-model="indexArray"
:show="showPicker"
@confirm="onConfirm"
@close="showPicker = false"
></u-picker>
</view>
</template>
<script>
export default {
data() {
return {
optionsArray: [
{ label: "汉族", value: "01" },
{ label: "蒙古族", value: "02" },
{ label: "回族", value: "03" }
],
indexArray: [0],
showPicker: false,
form: {
nationality: ''
}
}
},
methods: {
onConfirm(e) {
const index = e.value[0];
this.indexArray = [index];
this.form.nationality = this.optionsArray[index]?.value || '';
this.showPicker = false;
},
getSelectedLabel() {
const index = this.indexArray[0];
return this.optionsArray[index]?.label || '';
}
}
}
</script>
问题1: picker弹出后数据区域空白
:range而非:columns,或数据格式不是二维数组:columns="[dataArray]"格式问题2: 事件处理获取不到正确数据
e.detail.value而非e.valuee.value[0]获取索引问题3: 无法显示中文选项文本
keyName属性keyName="label"指定显示字段vue
<u-popup v-model="show" mode="bottom" height="500">
<view class="popup-content">
<!-- 弹窗内容 -->
</view>
</u-popup>
- 常用属性:
- v-model: 控制弹窗显示/隐藏
- mode: 弹出方向(bottom、top、left、right、center)
- height: 弹窗高度(bottom/top模式)
- width: 弹窗宽度(left/right模式)
- border-radius: 圆角大小
- closeable: 是否显示关闭图标
- mask-close: 是否允许点击遮罩关闭
- 详细文档参考:https://www.uviewui.com/components/popup.html
---
📝 维护: 移动端开发规范文档