feat(mobile): 实现全局路由守卫功能,自动处理用户未登录跳转
✨ 新增功能: - 全局路由守卫模块 (route-guard.js) - 多层次权限验证 (登录状态+Token存在+Token过期检查) - 自动拦截机制 (页面级+跳转级+HTTP级+应用级) - 401响应自动处理和跳转 - 路由守卫测试页面 🔧 技术实现: - checkPageAccess() 全局页面权限检查 - guardedNavigate/guardedSwitchTab 带权限检查的跳转 - handleUnauthorized() HTTP拦截器401处理 - 公开页面配置管理 - 优雅的错误提示和跳转逻辑 📱 页面集成: - App.vue 初始化路由守卫 - profile/workbench 页面添加权限检查 - index 页面优化跳转逻辑 - 新增测试页面验证功能 📚 文档完善: - 移动端路由守卫使用指南 - 详细的变更记录和技术说明 - 最佳实践和注意事项 🎯 核心效果: 用户登录失效或未登录时,访问任何需要权限的页面都自动跳转到登录页,提升安全性和用户体验。
This commit is contained in:
1 parent
5589cba249
commit
afe3e234e9
33 files changed
+4710
-43
No files matched your search
@@ -918,3 +918,25 @@ node [功能]-api-test.js
|
||||
- 补充缺失的测试代码路径信息
|
||||
- 按规范重新组织文档结构
|
||||
```
|
||||
|
||||
### 原型文件自动保存规范
|
||||
|
||||
1. **文件位置**:
|
||||
|
||||
- 移动端原型 → `docs/原型文件/移动端/`
|
||||
- Web 端原型 → `docs/原型文件/web端/`
|
||||
|
||||
2. **自动保存触发条件**:
|
||||
|
||||
- 新建原型文件时自动创建目录结构
|
||||
- 修改文件时自动保留历史版本(按`[功能]_v[版本]_[日期]`格式)
|
||||
|
||||
3. **文件权限预设**:
|
||||
|
||||
```bash
|
||||
chmod 755 docs/原型文件/ # 确保目录可写
|
||||
```
|
||||
|
||||
4. **冲突解决策略**:
|
||||
- 同名文件自动添加版本后缀
|
||||
- 中文路径自动转换为 UTF-8 编码
|
||||
+859
-12
@@ -356,7 +356,248 @@ export default {
|
||||
- **组件名称**: 使用 PascalCase (如: `NavBar`, `ServiceItem`)
|
||||
- **引用方式**: 支持驼峰和短横线 (如: `<NavBar>` 或 `<nav-bar>`)
|
||||
|
||||
## 5. 🌐 API 接口规范
|
||||
## 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)
|
||||
|
||||
@@ -410,20 +651,109 @@ export const api = {
|
||||
request({
|
||||
url: '/auth/login',
|
||||
method: 'POST',
|
||||
data,
|
||||
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: '/user/info',
|
||||
url: '/system/user/getInfo',
|
||||
method: 'GET',
|
||||
}),
|
||||
|
||||
// 获取首页数据
|
||||
getHomeData: () =>
|
||||
// 获取租户列表
|
||||
getTenantList: (isToken = false) =>
|
||||
request({
|
||||
url: '/home/data',
|
||||
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);
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
@@ -487,7 +817,518 @@ export const storage = {
|
||||
};
|
||||
```
|
||||
|
||||
## 6. 📱 页面配置规范
|
||||
## 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 配置
|
||||
|
||||
@@ -620,7 +1461,7 @@ export const storage = {
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 🎭 用户体验规范
|
||||
## 9. 🎭 用户体验规范
|
||||
|
||||
### 交互设计规范
|
||||
|
||||
@@ -730,7 +1571,7 @@ export const accessibilityUtils = {
|
||||
};
|
||||
```
|
||||
|
||||
## 8. 🚀 性能优化规范
|
||||
## 10. 🚀 性能优化规范
|
||||
|
||||
### 代码优化
|
||||
|
||||
@@ -801,7 +1642,7 @@ export const api = {
|
||||
};
|
||||
```
|
||||
|
||||
## 9. 🛠️ 开发工具配置
|
||||
## 11. 🛠️ 开发工具配置
|
||||
|
||||
### Vite 配置 (vite.config.js)
|
||||
|
||||
@@ -849,7 +1690,7 @@ module.exports = [
|
||||
];
|
||||
```
|
||||
|
||||
## 10. 📋 开发命令规范
|
||||
## 12. 📋 开发命令规范
|
||||
|
||||
### 常用命令
|
||||
|
||||
@@ -879,7 +1720,7 @@ npm run lint
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
## 11. ✅ 最佳实践总结
|
||||
## 13. ✅ 最佳实践总结
|
||||
|
||||
### 代码规范
|
||||
|
||||
@@ -937,3 +1778,9 @@ npm run lint:fix
|
||||
- 鸿蒙 App
|
||||
|
||||
遵循本规范能够确保代码质量、提升开发效率,并为用户提供优秀的移动端体验。
|
||||
|
||||
---
|
||||
|
||||
**📝 维护**: 移动端开发规范文档
|
||||
**🔄 更新**: 2025-01-03 (新增登录状态管理规范)
|
||||
**📁 文件**: .cursor/cursorrules-mobile.md
|
||||
+16
-1
@@ -80,5 +80,20 @@
|
||||
// 代码编辑时的预览控制
|
||||
"cursor.ai.edit.showPreview": false,
|
||||
"cursor.ai.edit.autoPreview": false,
|
||||
"cursor.ai.generation.showPreview": false
|
||||
"cursor.ai.generation.showPreview": false,
|
||||
"autoSave": {
|
||||
"prototypes": {
|
||||
"defaultPath": "docs/原型文件/",
|
||||
"autoCreateDir": true,
|
||||
"backupOldVersions": true,
|
||||
"encoding": "utf-8",
|
||||
"retryTimes": 3
|
||||
}
|
||||
},
|
||||
"fileEncoding": {
|
||||
"default": "utf-8",
|
||||
"override": {
|
||||
"docs/原型文件/": "gbk" // 针对中文路径的兼容方案
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,11 @@
|
||||
- **数据库变更** → `docs/设计文档/database/`
|
||||
- **业务逻辑变更** → `docs/业务模块/[模块名称]/`
|
||||
- **配置变更** → `docs/中间件配置/` 或 `docs/部署文档/`
|
||||
- **📱 移动端变更** → 移动端工程相关文档必须同步更新:
|
||||
- **移动端API新增/变更** → 相应的API文档必须补充移动端实现章节
|
||||
- **移动端页面开发** → `docs/开发文档/` 移动端相关文档
|
||||
- **移动端功能变更** → `docs/业务模块/[模块名称]/` 移动端部分
|
||||
- **移动端技术规范变更** → `.cursor/cursorrules-mobile.md`
|
||||
|
||||
#### 变更记录规范
|
||||
- **文件格式**: `docs/变更管理/code-adjust-record-YYYY-MM-DD-{username}.md`
|
||||
@@ -269,3 +274,19 @@ private Date updateTime; // 更新时间(含JSON格式化)
|
||||
**📝 维护**: 拆分完成于 2025-07-01,所有原有内容完整保留
|
||||
**📁 文件**: 5个专门规范文件 + 1个主配置文件
|
||||
**🔄 加载**: Cursor自动识别和应用相关规范
|
||||
|
||||
# 原型文件存放规范(2025-07-01 新增)
|
||||
- 所有后续生成的原型文件(如HTML原型、交互原型等),必须统一存放在 docs/原型文件/ 目录下的相应子目录中。
|
||||
- 子目录应按端(如 web、移动端、小程序等)或业务模块分类,确保结构清晰、便于查找和维护。
|
||||
- 禁止将原型文件存放在源码目录(如 web/src、mobile/src 等)或其他非文档目录。
|
||||
- 迁移或生成原型文件时,需同步更新目录结构说明文档(如 docs/原型文件/README.md )。
|
||||
|
||||
# uview-plus 图标组件规范(2025-07-01 新增)
|
||||
- 移动端UI开发中,图标组件统一使用 <u-icon />(uview-plus 3.x 官方标准),禁止使用 <up-icon /> 或 <uv-icon />。
|
||||
- 推荐通过 easycom 自动全局注册,无需手动 import。
|
||||
- 标准用法示例:
|
||||
```vue
|
||||
<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
|
||||
@@ -0,0 +1,63 @@
|
||||
# 原型文件管理规范
|
||||
|
||||
## 📁 目录结构
|
||||
```
|
||||
docs/原型文件/
|
||||
├── 移动端/ # 移动端原型文件(Axure/Figma/Sketch等)
|
||||
├── web端/ # Web端原型文件
|
||||
└── README.md # 本说明文档
|
||||
```
|
||||
|
||||
## 📝 管理规范
|
||||
|
||||
### 1. 文件命名规则
|
||||
- **格式**: `[功能模块]_v[版本号]_[日期].[扩展名]`
|
||||
*示例: `登录模块_v1.2_20250105.fig`*
|
||||
- **版本号**: 主版本.次版本(重大变更改主版本,小调整改次版本)
|
||||
- **日期格式**: YYYYMMDD
|
||||
|
||||
### 2. 版本控制要求
|
||||
- 每次修改必须保留历史版本(禁止覆盖)
|
||||
- 版本变更需在`更新日志`中记录
|
||||
|
||||
### 3. 文件类型
|
||||
- **推荐格式**:
|
||||
- Figma (.fig)
|
||||
- Axure (.rp)
|
||||
- 可交互HTML
|
||||
- PDF截图(需包含版本信息)
|
||||
|
||||
### 4. 关联文档
|
||||
- 每个原型文件必须关联到:
|
||||
- `docs/需求文档/`中的对应需求文档
|
||||
- `docs/业务模块/`中的相关业务说明
|
||||
|
||||
### 5. 更新流程
|
||||
```mermaid
|
||||
graph TD
|
||||
A[需求变更] --> B[更新原型]
|
||||
B --> C[版本号+1]
|
||||
C --> D[更新日志记录]
|
||||
D --> E[同步关联文档]
|
||||
```
|
||||
|
||||
## 🗂️ 子目录说明
|
||||
|
||||
### 移动端原型
|
||||
- 存放位置: `移动端/`
|
||||
- 特殊要求:
|
||||
- 需包含各屏幕尺寸适配方案
|
||||
- 标注全局手势操作
|
||||
- 提供标准组件库文件
|
||||
|
||||
### Web端原型
|
||||
- 存放位置: `web端/`
|
||||
- 特殊要求:
|
||||
- 注明响应式断点
|
||||
- 包含权限状态展示
|
||||
- 提供配色规范文件
|
||||
|
||||
## 📅 更新日志
|
||||
| 版本 | 日期 | 修改人 | 变更说明 |
|
||||
|------|------------|----------|--------------------------|
|
||||
| v1.0 | 2025-01-05 | 系统生成 | 初始创建文档 |
|
||||
@@ -0,0 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>校园管理系统 - 移动端登录</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
/>
|
||||
<style>
|
||||
.brand-blue {
|
||||
background-color: rgb(93, 135, 255);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen flex items-center justify-center p-4">
|
||||
<div class="w-full max-w-md bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div class="brand-blue p-6 text-center">
|
||||
<i class="fas fa-graduation-cap text-white text-4xl mb-2"></i>
|
||||
<h1 class="text-2xl font-bold text-white">校园管理系统</h1>
|
||||
</div>
|
||||
<div class="p-8">
|
||||
<form>
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-medium mb-2">
|
||||
<i class="fas fa-user mr-2 text-blue-500"></i>
|
||||
账号
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入学工号"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-8">
|
||||
<label class="block text-gray-700 text-sm font-medium mb-2">
|
||||
<i class="fas fa-lock mr-2 text-blue-500"></i>
|
||||
密码
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full brand-blue text-white py-3 px-4 rounded-lg font-medium text-lg hover:bg-blue-600 transition shadow-md"
|
||||
>
|
||||
<i class="fas fa-sign-in-alt mr-2"></i>
|
||||
登录
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-6 py-4 text-center text-gray-500 text-sm">
|
||||
© 2025 校园管理系统 v3.0
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,77 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<title>移动端登录原型</title>
|
||||
<!-- Tailwind CSS CDN -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- FontAwesome CDN -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"
|
||||
/>
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #5d87ff 0%, #a3bfff 100%);
|
||||
}
|
||||
.brand-color {
|
||||
color: #5d87ff;
|
||||
}
|
||||
.brand-bg {
|
||||
background-color: #5d87ff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body
|
||||
class="min-h-screen flex flex-col justify-center items-center bg-gradient-to-br from-[#5D87FF] to-[#a3bfff]"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-xs mx-auto bg-white/80 rounded-2xl shadow-lg p-8 flex flex-col items-center"
|
||||
>
|
||||
<!-- Logo -->
|
||||
<div class="mb-6">
|
||||
<i class="fa-solid fa-graduation-cap text-5xl brand-color"></i>
|
||||
</div>
|
||||
<!-- 标题 -->
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-2">智慧校园</h1>
|
||||
<p class="text-sm text-gray-500 mb-8">杭州第一技师学院</p>
|
||||
<!-- 登录表单 -->
|
||||
<form class="w-full flex flex-col gap-4">
|
||||
<div class="relative">
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
|
||||
<i class="fa-regular fa-user"></i>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="账号"
|
||||
class="pl-10 pr-3 py-2 w-full rounded-lg border border-gray-200 focus:border-[#5D87FF] focus:ring-2 focus:ring-[#5D87FF]/30 outline-none bg-white text-gray-800 transition"
|
||||
/>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
|
||||
<i class="fa-solid fa-lock"></i>
|
||||
</span>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
class="pl-10 pr-3 py-2 w-full rounded-lg border border-gray-200 focus:border-[#5D87FF] focus:ring-2 focus:ring-[#5D87FF]/30 outline-none bg-white text-gray-800 transition"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full py-2 mt-2 rounded-lg brand-bg text-white font-semibold text-lg shadow hover:opacity-90 transition"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
</form>
|
||||
<!-- 版权信息 -->
|
||||
<div class="mt-8 text-xs text-gray-400 text-center w-full">
|
||||
© 2025 杭州第一技师学院 All Rights Reserved.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,391 @@
|
||||
# 代码调整记录 - 2025-01-03 - wuzilong
|
||||
|
||||
## 📋 变更时间倒序排列
|
||||
|
||||
### 🧩 登录页面输入组件升级为 uview-plus - 2025-01-03 18:45:00
|
||||
|
||||
**变更类型**: 组件升级
|
||||
**影响范围**: 移动端登录页面
|
||||
|
||||
#### 变更内容
|
||||
|
||||
1. **替换原生 input 为 uview-plus 组件**
|
||||
|
||||
- 账号输入框:使用`u-input`组件,添加用户图标前缀
|
||||
- 密码输入框:使用`u-input`组件,添加密码锁图标前缀
|
||||
- 登录按钮:使用`u-button`组件,保持图标+文字布局
|
||||
|
||||
2. **保持 SVG 图标系统**
|
||||
|
||||
- 继续使用`icon-user`、`icon-lock`、`icon-login`图标
|
||||
- 通过`template #prefix`插槽添加前缀图标
|
||||
- 保持原有的颜色和视觉效果
|
||||
|
||||
3. **样式配置优化**
|
||||
- 通过`inputStyle`和`buttonStyle`对象配置组件样式
|
||||
- 保持与原设计一致的视觉效果
|
||||
- 优化图标布局和间距
|
||||
|
||||
#### 技术细节
|
||||
|
||||
- **组件升级**: 原生`input` → `u-input`,原生`button` → `u-button`
|
||||
- **图标集成**: 使用`template #prefix`插槽集成 SVG 图标
|
||||
- **样式配置**: 通过`:custom-style`属性传递样式对象
|
||||
- **交互保持**: 保持原有的表单验证和提示逻辑
|
||||
|
||||
#### 文件变更
|
||||
|
||||
- `mobile/src/pages/login/index.vue`: 完整替换输入组件和按钮组件
|
||||
|
||||
#### 预期效果
|
||||
|
||||
- 更好的组件一致性,统一使用 uview-plus 组件库
|
||||
- 保持原有视觉设计和用户体验
|
||||
- 提升组件的可维护性和扩展性
|
||||
|
||||
---
|
||||
|
||||
### 🎨 移动端图标使用规范规范化 - 2025-01-03 18:30:00
|
||||
|
||||
**变更类型**: 规范文档更新
|
||||
**影响范围**: 移动端开发规范、图标系统
|
||||
|
||||
#### 变更内容
|
||||
|
||||
1. **将 SVG Data URI 图标方案写入 cursorrules 规范**
|
||||
|
||||
- 在`.cursor/cursorrules-mobile.md`中添加第 5 章"图标使用规范"
|
||||
- 详细说明 SVG Data URI 方案的背景、优势和使用方法
|
||||
- 提供完整的图标库列表(17 个图标)
|
||||
- 明确强制性规范要求
|
||||
|
||||
2. **规范核心要求**
|
||||
|
||||
- 禁止使用 uview-plus 的 u-icon 组件
|
||||
- 统一使用 SVG Data URI 方案
|
||||
- 图标类名必须使用`icon-`前缀
|
||||
- 通过 CSS filter 实现颜色自定义
|
||||
|
||||
3. **更新章节编号**
|
||||
- 调整原有章节编号(第 5 章及以后)
|
||||
- 确保规范文档结构清晰
|
||||
|
||||
#### 技术细节
|
||||
|
||||
- **配置文件位置**: `mobile/src/static/fonts/iconfont.css`
|
||||
- **全局引入**: 在`main.js`中导入样式
|
||||
- **可用图标**: 基础图标 4 个、操作图标 5 个、导航图标 4 个、联系图标 2 个
|
||||
- **颜色方案**: 提供主题色、成功色、警告色、错误色、灰色的 filter 值
|
||||
|
||||
#### 文档更新
|
||||
|
||||
- 更新知识库 memory,记录规范化的图标方案
|
||||
- 确保后续移动端开发都遵循统一的图标使用规范
|
||||
|
||||
#### 预期效果
|
||||
|
||||
- 后续 Cursor AI 生成移动端代码时自动采用 SVG Data URI 图标方案
|
||||
- 解决 uview-plus 图标显示异常问题
|
||||
- 提供稳定、高效、跨平台的图标解决方案
|
||||
|
||||
---
|
||||
|
||||
## 🎉 SVG 图标方案最终成功 - 2025-01-03 18:15:42
|
||||
|
||||
### 📋 变更概述
|
||||
|
||||
**目的**: 经过多次尝试(Font Awesome、iconfont 项目),最终采用 SVG Data URI 方案成功解决图标显示问题,实现稳定可靠的图标系统。
|
||||
|
||||
### 🔧 最终技术实现
|
||||
|
||||
#### 1. **SVG Data URI 图标** (`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");
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. **技术优势**
|
||||
|
||||
- ✅ **零依赖**: 不依赖任何外部字体文件或 CDN
|
||||
- ✅ **加载稳定**: SVG 直接嵌入 CSS,无 404 错误
|
||||
- ✅ **跨平台兼容**: 所有设备都能正常显示
|
||||
- ✅ **性能优异**: 无网络请求,加载速度快
|
||||
- ✅ **可定制**: 通过 CSS filter 调整颜色
|
||||
|
||||
#### 3. **颜色自定义示例**
|
||||
|
||||
```css
|
||||
/* 主题色 #5d87ff */
|
||||
.icon-custom {
|
||||
filter: brightness(0) saturate(100%) invert(38%) sepia(87%) saturate(6400%) hue-rotate(225deg)
|
||||
brightness(104%) contrast(104%);
|
||||
}
|
||||
|
||||
/* 灰色 #b0b8c9 */
|
||||
.icon-gray {
|
||||
filter: brightness(0) saturate(100%) invert(67%) sepia(8%) saturate(1179%) hue-rotate(195deg)
|
||||
brightness(95%) contrast(88%);
|
||||
}
|
||||
```
|
||||
|
||||
### 📊 解决方案对比
|
||||
|
||||
| 方案 | 依赖性 | 稳定性 | 加载速度 | 自定义性 | 兼容性 |
|
||||
| ---------------- | ----------- | ----------- | -------- | ------------- | ------- |
|
||||
| **SVG Data URI** | 🟢 无依赖 | 🟢 100%稳定 | 🟢 极快 | 🟢 CSS filter | 🟢 完美 |
|
||||
| Font Awesome CDN | 🔴 CDN 依赖 | 🟡 网络依赖 | 🟡 一般 | 🟢 CSS 属性 | 🟢 良好 |
|
||||
| iconfont 项目 | 🔴 字体文件 | 🔴 404 错误 | 🔴 失败 | 🟢 CSS 属性 | 🔴 失败 |
|
||||
| Emoji | 🟢 无依赖 | 🟡 设备差异 | 🟢 快 | 🔴 无法定制 | 🟡 一般 |
|
||||
|
||||
### 🎯 成功验证
|
||||
|
||||
1. **图标演示页面**: http://localhost:5173/#/pages/iconfont-demo/index ✅
|
||||
2. **登录页面图标**: http://localhost:5173/#/pages/login/index ✅
|
||||
3. **17 个图标**: 基础、操作、导航、联系四大类全部正常显示 ✅
|
||||
|
||||
### 📱 最终使用方式
|
||||
|
||||
```vue
|
||||
<!-- 基础用法 -->
|
||||
<text class="iconfont icon-user"></text>
|
||||
<text class="iconfont icon-lock"></text>
|
||||
|
||||
<!-- 自定义大小和颜色 -->
|
||||
<text
|
||||
class="iconfont icon-login"
|
||||
:style="{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
filter: 'brightness(0) saturate(100%) invert(38%) sepia(87%) saturate(6400%) hue-rotate(225deg) brightness(104%) contrast(104%)',
|
||||
}"
|
||||
></text>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 引入 iconfont.cn 图标库 - 2025-01-03 17:30:15
|
||||
|
||||
### 📋 变更概述
|
||||
|
||||
**目的**: 在移动端 uni-app 项目中引入 iconfont.cn 图标字体库,解决 uview-plus 图标显示异常问题,提供稳定的矢量图标解决方案。
|
||||
|
||||
### 🔧 修改内容
|
||||
|
||||
#### 1. **创建 iconfont 字体配置** (`mobile/src/static/fonts/iconfont.css`)
|
||||
|
||||
**新增完整的 iconfont.css 文件**:
|
||||
|
||||
```css
|
||||
@font-face {
|
||||
font-family: 'iconfont'; /* Project id 4793979 */
|
||||
src: url('//at.alicdn.com/t/c/font_4793979_9m3yqql3ybj.woff2?t=1704153600') format('woff2'), url('//at.alicdn.com/t/c/font_4793979_9m3yqql3ybj.woff?t=1704153600')
|
||||
format('woff'),
|
||||
url('//at.alicdn.com/t/c/font_4793979_9m3yqql3ybj.ttf?t=1704153600') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: 'iconfont' !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* 包含17个常用图标类定义 */
|
||||
.icon-user:before {
|
||||
content: '\e7ae';
|
||||
} /* 用户图标 */
|
||||
.icon-lock:before {
|
||||
content: '\e60e';
|
||||
} /* 密码锁图标 */
|
||||
.icon-eye:before {
|
||||
content: '\e61c';
|
||||
} /* 眼睛图标 */
|
||||
.icon-login:before {
|
||||
content: '\e612';
|
||||
} /* 登录图标 */
|
||||
/* ... 更多图标 */
|
||||
```
|
||||
|
||||
#### 2. **全局样式引入** (`mobile/src/main.js`)
|
||||
|
||||
**添加 iconfont 样式导入**:
|
||||
|
||||
```javascript
|
||||
// 导入 iconfont 样式
|
||||
import '@/static/fonts/iconfont.css';
|
||||
```
|
||||
|
||||
#### 3. **登录页面图标更新** (`mobile/src/pages/login/index.vue`)
|
||||
|
||||
**替换 emoji 为 iconfont 图标**:
|
||||
|
||||
```vue
|
||||
<!-- 之前使用emoji -->
|
||||
<text class="input-icon-emoji">👤</text>
|
||||
<text class="input-icon-emoji">🔒</text>
|
||||
|
||||
<!-- 现在使用iconfont -->
|
||||
<text class="iconfont icon-user input-icon"></text>
|
||||
<text class="iconfont icon-lock input-icon"></text>
|
||||
|
||||
<!-- 登录按钮添加图标 -->
|
||||
<button class="login-btn" @click="onLogin">
|
||||
<text class="iconfont icon-login"></text>
|
||||
<text class="login-btn-text">登录</text>
|
||||
</button>
|
||||
```
|
||||
|
||||
#### 4. **创建图标演示页面** (`mobile/src/pages/iconfont-demo/index.vue`)
|
||||
|
||||
**功能特性**:
|
||||
|
||||
- 展示所有 17 个可用图标
|
||||
- 按分类展示(基础、操作、导航、联系)
|
||||
- 包含使用代码示例
|
||||
- 响应式网格布局
|
||||
- 图标悬停效果
|
||||
|
||||
#### 5. **页面路由配置** (`mobile/src/pages.json`)
|
||||
|
||||
**新增演示页面路由**:
|
||||
|
||||
```json
|
||||
{
|
||||
"path": "pages/iconfont-demo/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "图标演示",
|
||||
"navigationBarBackgroundColor": "#5d87ff",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. **创建使用指南文档** (`docs/开发文档/移动端iconfont图标使用指南.md`)
|
||||
|
||||
**包含内容**:
|
||||
|
||||
- 完整的安装配置说明
|
||||
- 17 个图标的详细列表(类名、说明、编码)
|
||||
- 多种使用场景的代码示例
|
||||
- 样式自定义指南
|
||||
- 扩展图标的方法
|
||||
- 兼容性说明和最佳实践
|
||||
|
||||
### 📊 图标库特性
|
||||
|
||||
#### 可用图标列表 (17 个)
|
||||
|
||||
**基础图标**: user, lock, eye, eye-close
|
||||
**操作图标**: login, search, add, check, close
|
||||
**导航图标**: home, message, setting, arrow-right
|
||||
**联系图标**: phone, mail
|
||||
|
||||
#### 技术优势
|
||||
|
||||
1. **跨平台兼容**: 支持 H5、微信小程序、App、HarmonyOS
|
||||
2. **矢量图标**: 无限缩放不失真,适配各种屏幕密度
|
||||
3. **性能优化**: CDN 加载,字体平滑处理
|
||||
4. **灵活定制**: CSS 自定义大小、颜色、样式
|
||||
|
||||
### 🎯 解决的问题
|
||||
|
||||
1. **uview-plus 图标异常**: u-icon 组件显示为横线的问题彻底解决
|
||||
2. **emoji 兼容性**: 不同设备 emoji 显示不一致的问题
|
||||
3. **图标扩展性**: 提供更丰富的图标选择和自定义能力
|
||||
4. **字体文件 404 问题**: 使用 SVG Data URI 避免外部字体文件依赖
|
||||
|
||||
### 📱 使用方式
|
||||
|
||||
```vue
|
||||
<!-- 基础用法 -->
|
||||
<text class="iconfont icon-user"></text>
|
||||
|
||||
<!-- 自定义样式 -->
|
||||
<text class="iconfont icon-home custom-icon"></text>
|
||||
|
||||
<!-- 登录表单应用 -->
|
||||
<view class="input-group">
|
||||
<text class="iconfont icon-user input-icon"></text>
|
||||
<input class="login-input" placeholder="账号" />
|
||||
</view>
|
||||
```
|
||||
|
||||
### 📝 文档覆盖
|
||||
|
||||
- ✅ **技术文档**: 完整的使用指南和 API 参考
|
||||
- ✅ **演示页面**: 可视化的图标展示和代码示例
|
||||
- ✅ **变更记录**: 详细的实现过程和配置说明
|
||||
- ✅ **最佳实践**: 命名规范、性能优化、回退方案
|
||||
|
||||
### 🚀 后续计划
|
||||
|
||||
1. **图标扩展**: 根据业务需求添加更多图标
|
||||
2. **主题适配**: 适配暗色模式和多主题切换
|
||||
3. **组件封装**: 封装常用的图标组件,简化使用
|
||||
|
||||
---
|
||||
|
||||
## 📱 完善移动端文档同步规范 - 2025-01-03 16:45:23
|
||||
|
||||
### 📋 变更概述
|
||||
|
||||
**目的**: 完善.cursorrules 主配置文件,补充移动端工程的强制性文档同步更新规范,确保移动端代码变更时也能及时更新相关文档。
|
||||
|
||||
### 🔧 修改内容
|
||||
|
||||
#### 1. **主配置文件更新** (`.cursorrules`)
|
||||
|
||||
**位置**: "强制性文档同步更新" → "必须更新的文档" 部分
|
||||
|
||||
**新增内容**:
|
||||
|
||||
```markdown
|
||||
- **📱 移动端变更** → 移动端工程相关文档必须同步更新:
|
||||
- **移动端 API 新增/变更** → 相应的 API 文档必须补充移动端实现章节
|
||||
- **移动端页面开发** → `docs/开发文档/` 移动端相关文档
|
||||
- **移动端功能变更** → `docs/业务模块/[模块名称]/` 移动端部分
|
||||
- **移动端技术规范变更** → `.cursor/cursorrules-mobile.md`
|
||||
```
|
||||
|
||||
### 📊 规范意义
|
||||
|
||||
1. **统一标准**: 移动端、前端、后端都遵循相同的文档同步要求
|
||||
2. **防止遗漏**: 明确规定移动端变更时必须更新的文档类型
|
||||
3. **完整覆盖**:
|
||||
- API 变更 → API 文档补充移动端实现
|
||||
- 页面开发 → 开发文档记录
|
||||
- 功能变更 → 业务模块文档更新
|
||||
- 技术规范 → cursorrules 文档维护
|
||||
|
||||
### 📝 强制执行要求
|
||||
|
||||
- **移动端 API 新增/变更**: 必须在对应的 API 文档中补充移动端实现章节
|
||||
- **移动端页面开发**: 必须在`docs/开发文档/`中记录相关文档
|
||||
- **移动端功能变更**: 必须更新`docs/业务模块/[模块名称]/`中的移动端部分
|
||||
- **移动端技术规范变更**: 必须同步更新`.cursor/cursorrules-mobile.md`
|
||||
|
||||
### 🎯 价值目标
|
||||
|
||||
- 📚 **文档完整性**: 确保移动端变更有完整的文档记录
|
||||
- 🔄 **规范一致性**: 所有技术栈都遵循统一的文档管理规范
|
||||
- 📖 **知识传承**: 移动端开发经验能够有效积累和传承
|
||||
- 🚀 **团队效率**: 为后续移动端开发提供准确的文档支持
|
||||
|
||||
---
|
||||
|
||||
## 📱 移动端登录页面完整开发 - 2025-01-03 15:30:12
|
||||
@@ -1,5 +1,239 @@
|
||||
# 代码调整记录 - 2025-07-01 - wuzilong
|
||||
|
||||
## 2025-07-02 19:30:00 - 实现全局路由守卫功能
|
||||
|
||||
### 🎯 变更目标
|
||||
|
||||
- 实现用户登录失效或未登录时,访问任何需要权限的页面都自动跳转到登录页
|
||||
- 建立完整的路由权限控制体系
|
||||
- 提供多层次的权限检查机制
|
||||
- 增强用户体验和系统安全性
|
||||
|
||||
### 🔧 修改内容
|
||||
|
||||
**1. 新增全局路由守卫模块**
|
||||
|
||||
文件:`mobile/src/utils/route-guard.js`
|
||||
|
||||
```javascript
|
||||
// 核心功能
|
||||
- checkPageAccess() // 全局页面权限检查
|
||||
- guardedNavigate() // 带权限检查的页面跳转
|
||||
- guardedSwitchTab() // 带权限检查的Tab切换
|
||||
- routeGuardMixin // 页面混入,自动权限检查
|
||||
- initRouteGuard() // 初始化全局路由守卫
|
||||
|
||||
// 配置管理
|
||||
- PUBLIC_PAGES: ['/pages/login/index', '/pages/index/index'] // 公开页面列表
|
||||
- LOGIN_PAGE: '/pages/login/index' // 登录页路径
|
||||
```
|
||||
|
||||
**功能特性**:
|
||||
- ✅ 自动检测当前页面路径
|
||||
- ✅ 区分公开页面和受保护页面
|
||||
- ✅ 多重登录状态验证(isLoggedIn + Token存在 + Token未过期)
|
||||
- ✅ 优雅的错误提示和跳转
|
||||
- ✅ 支持页面加载和显示时的权限检查
|
||||
- ✅ 提供页面混入和手动调用两种使用方式
|
||||
|
||||
**2. 修改应用初始化**
|
||||
|
||||
文件:`mobile/src/App.vue`
|
||||
|
||||
```javascript
|
||||
import { initRouteGuard } from '@/utils/route-guard';
|
||||
|
||||
export default {
|
||||
onLaunch: function () {
|
||||
console.log('App Launch');
|
||||
// 初始化全局路由守卫
|
||||
initRouteGuard();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**3. 更新页面权限检查**
|
||||
|
||||
文件:`mobile/src/pages/profile/index.vue`
|
||||
|
||||
```javascript
|
||||
import { checkPageAccess } from '@/utils/route-guard';
|
||||
|
||||
// 页面生命周期权限检查
|
||||
onLoad() {
|
||||
if (!checkPageAccess()) return;
|
||||
// 其他初始化逻辑...
|
||||
},
|
||||
onShow() {
|
||||
if (!checkPageAccess()) return;
|
||||
// 刷新用户信息...
|
||||
}
|
||||
```
|
||||
|
||||
文件:`mobile/src/pages/workbench/index.vue`
|
||||
|
||||
```javascript
|
||||
import { checkPageAccess } from '@/utils/route-guard';
|
||||
|
||||
// 同样的权限检查模式
|
||||
onLoad() {
|
||||
if (!checkPageAccess()) return;
|
||||
this.checkLoginStatus();
|
||||
},
|
||||
onShow() {
|
||||
if (!checkPageAccess()) return;
|
||||
}
|
||||
```
|
||||
|
||||
**4. 优化首页跳转逻辑**
|
||||
|
||||
文件:`mobile/src/pages/index/index.vue`
|
||||
|
||||
```javascript
|
||||
import { guardedSwitchTab } from '@/utils/route-guard';
|
||||
|
||||
// 使用带权限检查的Tab切换
|
||||
navigateToService() {
|
||||
guardedSwitchTab('/pages/workbench/index');
|
||||
}
|
||||
```
|
||||
|
||||
**5. 增强HTTP请求拦截器**
|
||||
|
||||
文件:`mobile/src/utils/request.js`
|
||||
|
||||
```javascript
|
||||
// 401响应自动处理
|
||||
function handleUnauthorized(message = '登录已失效,请重新登录') {
|
||||
// 清除登录信息
|
||||
uni.removeStorageSync('access_token');
|
||||
uni.removeStorageSync('userInfo');
|
||||
uni.removeStorageSync('isLoggedIn');
|
||||
|
||||
// 显示提示并跳转
|
||||
uni.showToast({ title: message, icon: 'none' });
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/login/index' });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// HTTP响应拦截
|
||||
success: (res) => {
|
||||
if (res.data.code === 401 || res.statusCode === 401) {
|
||||
handleUnauthorized(res.data.msg || '登录已失效,请重新登录');
|
||||
reject(res.data);
|
||||
}
|
||||
// 其他处理...
|
||||
}
|
||||
```
|
||||
|
||||
**6. 新增测试页面**
|
||||
|
||||
文件:`mobile/src/pages/test-route/index.vue`
|
||||
|
||||
- 🧪 路由守卫功能测试页面
|
||||
- 📊 实时显示登录状态、Token状态、用户信息
|
||||
- 🔧 提供测试按钮:清除Token、带守卫跳转等
|
||||
- 📋 详细的测试日志显示
|
||||
|
||||
### 📝 技术细节
|
||||
|
||||
**权限检查流程**:
|
||||
|
||||
1. **页面路径检查** → 判断是否为公开页面
|
||||
2. **登录状态检查** → userStorage.isLoggedIn()
|
||||
3. **Token存在检查** → userStorage.getToken()
|
||||
4. **Token过期检查** → userState.checkTokenExpiration()
|
||||
|
||||
**多层次拦截机制**:
|
||||
|
||||
1. **页面级拦截** → onLoad/onShow 中调用 checkPageAccess()
|
||||
2. **跳转级拦截** → guardedNavigate/guardedSwitchTab 预检查
|
||||
3. **HTTP级拦截** → 401响应自动处理
|
||||
4. **应用级拦截** → App进入前台时状态检查
|
||||
|
||||
**用户体验优化**:
|
||||
|
||||
- ✅ 友好的错误提示信息
|
||||
- ✅ 延迟跳转确保Toast显示
|
||||
- ✅ 使用reLaunch清除页面栈
|
||||
- ✅ 失败时自动尝试备用跳转方式
|
||||
|
||||
### ✅ 验证结果
|
||||
|
||||
**权限控制测试**:
|
||||
- ✅ 未登录访问受保护页面 → 自动跳转登录页
|
||||
- ✅ Token过期访问受保护页面 → 清除信息并跳转
|
||||
- ✅ 登录后正常访问所有页面
|
||||
- ✅ 公开页面无论何时都可访问
|
||||
|
||||
**跳转功能测试**:
|
||||
- ✅ guardedNavigate 正确拦截未授权跳转
|
||||
- ✅ guardedSwitchTab 正确处理Tab切换
|
||||
- ✅ HTTP 401响应自动清理和跳转
|
||||
|
||||
**兼容性测试**:
|
||||
- ✅ 不影响现有登录流程
|
||||
- ✅ 与现有用户状态管理兼容
|
||||
- ✅ 页面加载性能无影响
|
||||
|
||||
### 🎯 影响评估
|
||||
|
||||
**安全性提升**:
|
||||
- 🔒 完全防止未授权页面访问
|
||||
- 🔒 自动处理Token失效情况
|
||||
- 🔒 多层次权限检查防止绕过
|
||||
|
||||
**用户体验改善**:
|
||||
- 🎨 无感知的权限检查
|
||||
- 🎨 友好的错误提示
|
||||
- 🎨 自动跳转减少用户操作
|
||||
|
||||
**开发效率提升**:
|
||||
- 🚀 统一的权限检查工具
|
||||
- 🚀 简单的页面集成方式
|
||||
- 🚀 完善的测试工具
|
||||
|
||||
### 💡 最佳实践建议
|
||||
|
||||
**页面开发规范**:
|
||||
```javascript
|
||||
// 在需要权限的页面中
|
||||
import { checkPageAccess } from '@/utils/route-guard';
|
||||
|
||||
onLoad() {
|
||||
if (!checkPageAccess()) return;
|
||||
// 页面初始化逻辑
|
||||
},
|
||||
onShow() {
|
||||
if (!checkPageAccess()) return;
|
||||
// 页面显示逻辑
|
||||
}
|
||||
```
|
||||
|
||||
**页面跳转规范**:
|
||||
```javascript
|
||||
// 使用带权限检查的跳转
|
||||
import { guardedNavigate, guardedSwitchTab } from '@/utils/route-guard';
|
||||
|
||||
// 普通页面跳转
|
||||
guardedNavigate('/pages/some-page/index');
|
||||
|
||||
// Tab页面切换
|
||||
guardedSwitchTab('/pages/workbench/index');
|
||||
```
|
||||
|
||||
**公开页面配置**:
|
||||
```javascript
|
||||
// 在 route-guard.js 中配置公开页面
|
||||
const PUBLIC_PAGES = [
|
||||
'/pages/login/index', // 登录页
|
||||
'/pages/index/index' // 首页(功能受限)
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2025-07-02 18:00:00 - 解决移动端 Buffer 兼容性问题
|
||||
|
||||
### 🐛 问题描述
|
||||
|
||||
+193
-2
@@ -460,6 +460,197 @@ admin 用户具有系统最高权限`*:*:*`,可以访问所有功能模块和
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 📱 移动端登录实现
|
||||
|
||||
### 移动端特殊配置
|
||||
|
||||
移动端使用uni-app框架,登录实现与web端保持API兼容,但在实现细节上有所差异:
|
||||
|
||||
**网络请求配置**:
|
||||
```javascript
|
||||
// mobile/src/utils/request.js
|
||||
const baseURL = 'http://localhost:8080'
|
||||
|
||||
const request = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = getToken()
|
||||
|
||||
uni.request({
|
||||
url: baseURL + options.url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token,
|
||||
'clientid': 'e5cd7e4891bf95d1d19206ce24a7b32e',
|
||||
'referer': baseURL,
|
||||
...options.header,
|
||||
},
|
||||
// 响应处理逻辑...
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**登录API调用**:
|
||||
```javascript
|
||||
// 移动端登录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'
|
||||
}),
|
||||
|
||||
// 其他API...
|
||||
}
|
||||
```
|
||||
|
||||
### 移动端用户信息缓存
|
||||
|
||||
**用户信息存储工具**:
|
||||
```javascript
|
||||
// mobile/src/utils/request.js
|
||||
export const userStorage = {
|
||||
// 保存登录信息
|
||||
saveLoginInfo: (loginResult, userInfo) => {
|
||||
try {
|
||||
uni.setStorageSync('access_token', loginResult.access_token)
|
||||
if (userInfo) {
|
||||
uni.setStorageSync('userInfo', JSON.stringify(userInfo))
|
||||
}
|
||||
uni.setStorageSync('isLoggedIn', true)
|
||||
} catch (e) {
|
||||
console.error('保存登录信息失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo: () => {
|
||||
try {
|
||||
const userInfo = uni.getStorageSync('userInfo')
|
||||
return userInfo ? JSON.parse(userInfo) : null
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
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')
|
||||
} catch (e) {
|
||||
console.error('清除登录信息失败:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 移动端登录流程
|
||||
|
||||
1. **应用启动** → 检查登录状态 → 未登录自动跳转登录页
|
||||
2. **用户登录** → 调用`/auth/login`API → 获取access_token
|
||||
3. **获取用户信息** → 调用`/system/user/getInfo`API → 获取用户详情
|
||||
4. **缓存信息** → 使用`userStorage.saveLoginInfo()`缓存登录信息
|
||||
5. **页面访问** → 所有页面自动检查登录状态
|
||||
6. **退出登录** → 清除本地缓存 → 跳转登录页
|
||||
|
||||
### 移动端页面入口配置
|
||||
|
||||
**pages.json配置**:
|
||||
```json
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录",
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarBackgroundColor": "transparent",
|
||||
"backgroundColor": "transparent"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 移动端登录状态验证
|
||||
|
||||
**全局登录验证**:
|
||||
```javascript
|
||||
// 在每个需要登录的页面中
|
||||
onLoad() {
|
||||
this.checkLoginStatus();
|
||||
},
|
||||
|
||||
methods: {
|
||||
checkLoginStatus() {
|
||||
if (!userStorage.isLoggedIn()) {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 移动端与web端差异
|
||||
|
||||
| 特性 | Web端 | 移动端 |
|
||||
|------|-------|--------|
|
||||
| 框架 | Vue 3 + TypeScript | Vue 3 + uni-app |
|
||||
| 存储 | localStorage | uni.storage |
|
||||
| 网络请求 | axios | uni.request |
|
||||
| 路由跳转 | router.push | uni.navigateTo/reLaunch |
|
||||
| UI组件 | Element Plus | uview-plus |
|
||||
| 加密方式 | AES+RSA | 支持,但简化实现 |
|
||||
|
||||
### 移动端登录页面特性
|
||||
|
||||
- ✅ **现代化UI**: 渐变背景 + 毛玻璃效果
|
||||
- ✅ **租户选择**: 支持多租户选择(与web端一致)
|
||||
- ✅ **表单验证**: 完整的输入验证和错误提示
|
||||
- ✅ **响应式设计**: 适配不同屏幕尺寸
|
||||
- ✅ **无障碍访问**: 支持键盘导航和屏幕阅读器
|
||||
- ✅ **安全存储**: 使用uni.storage安全存储用户信息
|
||||
|
||||
---
|
||||
|
||||
## 📞 联系方式
|
||||
|
||||
如有认证相关问题,请联系开发团队或查看项目仓库的 Issues。
|
||||
@@ -467,5 +658,5 @@ admin 用户具有系统最高权限`*:*:*`,可以访问所有功能模块和
|
||||
---
|
||||
|
||||
**📝 维护**: 用户认证登录 API 文档
|
||||
**🔄 更新**: 2025-07-01
|
||||
**�� 文件**: 用户认证登录 API.md
|
||||
**🔄 更新**: 2025-01-03
|
||||
**📁 文件**: 用户认证登录 API.md
|
||||
@@ -0,0 +1,351 @@
|
||||
# 移动端 iconfont 图标使用指南
|
||||
|
||||
## 📋 概述
|
||||
|
||||
本项目已集成 iconfont.cn 图标字体库,提供丰富的矢量图标,替代 emoji 和 uview-plus 图标,确保跨平台兼容性。
|
||||
|
||||
## 🎯 解决的问题
|
||||
|
||||
- **uview-plus 图标显示异常**:u-icon 组件在某些环境下显示为横线
|
||||
- **emoji 兼容性问题**:不同设备和系统 emoji 显示效果不一致
|
||||
- **图标扩展性**:iconfont 提供更丰富的图标选择
|
||||
|
||||
## 📦 安装配置
|
||||
|
||||
### 1. 字体文件配置
|
||||
|
||||
**文件位置**:`mobile/src/static/fonts/iconfont.css`
|
||||
|
||||
```css
|
||||
@font-face {
|
||||
font-family: 'iconfont';
|
||||
src: url('//at.alicdn.com/t/c/font_4793979_9m3yqql3ybj.woff2?t=1704153600') format('woff2'), url('//at.alicdn.com/t/c/font_4793979_9m3yqql3ybj.woff?t=1704153600')
|
||||
format('woff'),
|
||||
url('//at.alicdn.com/t/c/font_4793979_9m3yqql3ybj.ttf?t=1704153600') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: 'iconfont' !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 全局引入
|
||||
|
||||
**文件位置**:`mobile/src/main.js`
|
||||
|
||||
```javascript
|
||||
// 导入 iconfont 样式
|
||||
import '@/static/fonts/iconfont.css';
|
||||
```
|
||||
|
||||
## 🎨 可用图标列表
|
||||
|
||||
### 基础图标
|
||||
|
||||
| 图标 | 类名 | 说明 | 编码 |
|
||||
| ---- | ---------------- | -------- | ------- |
|
||||
| 👤 | `icon-user` | 用户 | `\e7ae` |
|
||||
| 🔒 | `icon-lock` | 锁定 | `\e60e` |
|
||||
| 👁️ | `icon-eye` | 显示密码 | `\e61c` |
|
||||
| 🙈 | `icon-eye-close` | 隐藏密码 | `\e61d` |
|
||||
|
||||
### 操作图标
|
||||
|
||||
| 图标 | 类名 | 说明 | 编码 |
|
||||
| ---- | ------------- | ---- | ------- |
|
||||
| 🚪 | `icon-login` | 登录 | `\e612` |
|
||||
| 🔍 | `icon-search` | 搜索 | `\e6b4` |
|
||||
| ➕ | `icon-add` | 添加 | `\e6a4` |
|
||||
| ✅ | `icon-check` | 确认 | `\e6a8` |
|
||||
| ❌ | `icon-close` | 关闭 | `\e6a9` |
|
||||
|
||||
### 导航图标
|
||||
|
||||
| 图标 | 类名 | 说明 | 编码 |
|
||||
| ---- | ------------------ | ------ | ------- |
|
||||
| 🏠 | `icon-home` | 首页 | `\e65e` |
|
||||
| 💬 | `icon-message` | 消息 | `\e6a7` |
|
||||
| ⚙️ | `icon-setting` | 设置 | `\e6b8` |
|
||||
| ➡️ | `icon-arrow-right` | 箭头右 | `\e6a3` |
|
||||
|
||||
### 联系图标
|
||||
|
||||
| 图标 | 类名 | 说明 | 编码 |
|
||||
| ---- | ------------ | ---- | ------- |
|
||||
| 📞 | `icon-phone` | 电话 | `\e69a` |
|
||||
| 📧 | `icon-mail` | 邮箱 | `\e692` |
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 1. 基础用法
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- 基础图标 -->
|
||||
<text class="iconfont icon-user"></text>
|
||||
<text class="iconfont icon-lock"></text>
|
||||
|
||||
<!-- 带样式的图标 -->
|
||||
<text class="iconfont icon-home custom-icon"></text>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.custom-icon {
|
||||
font-size: 24px;
|
||||
color: #5d87ff;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### 2. 登录表单中的应用
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view class="input-group">
|
||||
<!-- 用户名输入框 -->
|
||||
<text class="iconfont icon-user input-icon"></text>
|
||||
<input class="login-input" type="text" placeholder="账号" />
|
||||
</view>
|
||||
|
||||
<view class="input-group">
|
||||
<!-- 密码输入框 -->
|
||||
<text class="iconfont icon-lock input-icon"></text>
|
||||
<input class="login-input" type="password" placeholder="密码" />
|
||||
</view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button class="login-btn">
|
||||
<text class="iconfont icon-login"></text>
|
||||
<text>登录</text>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.input-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
z-index: 2;
|
||||
font-size: 18px;
|
||||
color: #b0b8c9;
|
||||
}
|
||||
|
||||
.login-input {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding: 0 12px 0 44px;
|
||||
border: 1px solid #e3e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: #5d87ff;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### 3. 动态图标切换
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view class="password-toggle">
|
||||
<text
|
||||
:class="['iconfont', showPassword ? 'icon-eye-close' : 'icon-eye']"
|
||||
@click="togglePassword"
|
||||
></text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showPassword: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
togglePassword() {
|
||||
this.showPassword = !this.showPassword;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
## 🎨 样式自定义
|
||||
|
||||
### 1. 基础样式属性
|
||||
|
||||
```css
|
||||
.iconfont {
|
||||
/* 字体大小 */
|
||||
font-size: 16px;
|
||||
|
||||
/* 颜色 */
|
||||
color: #333;
|
||||
|
||||
/* 其他样式 */
|
||||
opacity: 0.8;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 响应式设计
|
||||
|
||||
```css
|
||||
/* 小屏幕 */
|
||||
@media (max-width: 375px) {
|
||||
.iconfont {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 大屏幕 */
|
||||
@media (min-width: 768px) {
|
||||
.iconfont {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 主题适配
|
||||
|
||||
```css
|
||||
/* 亮色主题 */
|
||||
.light-theme .iconfont {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 暗色主题 */
|
||||
.dark-theme .iconfont {
|
||||
color: #fff;
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 扩展图标
|
||||
|
||||
### 1. 从 iconfont.cn 添加新图标
|
||||
|
||||
1. **访问项目**:打开 [iconfont.cn](https://www.iconfont.cn/) 的项目页面
|
||||
2. **选择图标**:浏览并添加需要的图标到项目
|
||||
3. **生成代码**:下载新的字体文件和 CSS 代码
|
||||
4. **更新项目**:替换 `iconfont.css` 中的字体链接和图标类名
|
||||
|
||||
### 2. 图标命名规范
|
||||
|
||||
```css
|
||||
/* 建议的命名格式 */
|
||||
.icon-[功能]-[状态]
|
||||
|
||||
例如:
|
||||
.icon-user /* 用户图标 */
|
||||
.icon-eye-open /* 眼睛打开 */
|
||||
.icon-eye-close /* 眼睛关闭 */
|
||||
.icon-arrow-left /* 左箭头 */
|
||||
.icon-arrow-right; /* 右箭头 */
|
||||
```
|
||||
|
||||
## 📱 演示页面
|
||||
|
||||
**页面位置**:`mobile/src/pages/iconfont-demo/index.vue`
|
||||
|
||||
访问路径:`/pages/iconfont-demo/index`
|
||||
|
||||
演示页面包含:
|
||||
|
||||
- 所有可用图标的预览
|
||||
- 图标分类展示(基础、操作、导航、联系)
|
||||
- 使用代码示例
|
||||
- 自定义样式演示
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 1. 兼容性
|
||||
|
||||
- ✅ **支持平台**:H5、微信小程序、App、支付宝小程序
|
||||
- ✅ **浏览器支持**:Chrome、Safari、Firefox、Edge
|
||||
- ✅ **设备支持**:iOS、Android、HarmonyOS
|
||||
|
||||
### 2. 性能优化
|
||||
|
||||
```css
|
||||
/* 字体预加载 */
|
||||
.iconfont {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 缓存策略
|
||||
|
||||
- 字体文件使用 CDN 加载,自动缓存
|
||||
- 本地 CSS 文件确保离线可用
|
||||
- 建议定期更新字体链接时间戳
|
||||
|
||||
### 4. 回退方案
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- 图标 + 文字回退 -->
|
||||
<view class="icon-with-fallback">
|
||||
<text class="iconfont icon-user"></text>
|
||||
<text class="icon-fallback">用户</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.icon-fallback {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 当字体加载失败时显示文字 */
|
||||
@font-face {
|
||||
font-family: 'iconfont-fallback';
|
||||
src: local('Arial'), local('sans-serif');
|
||||
}
|
||||
|
||||
.iconfont:before {
|
||||
font-family: 'iconfont', 'iconfont-fallback';
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 📚 相关文件
|
||||
|
||||
- **样式文件**:`mobile/src/static/fonts/iconfont.css`
|
||||
- **演示页面**:`mobile/src/pages/iconfont-demo/index.vue`
|
||||
- **登录页面**:`mobile/src/pages/login/index.vue`
|
||||
- **配置文件**:`mobile/src/main.js`
|
||||
|
||||
## 🎯 最佳实践
|
||||
|
||||
1. **统一使用**:项目中统一使用 iconfont,避免混用多种图标方案
|
||||
2. **语义化命名**:使用有意义的类名,便于维护
|
||||
3. **适当大小**:根据使用场景选择合适的字体大小
|
||||
4. **颜色一致**:遵循设计规范,保持图标颜色的一致性
|
||||
5. **性能考虑**:避免一次性加载过多不使用的图标
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
- **2025-07-01**:初始版本,集成 iconfont 字体库
|
||||
- **预计扩展**:根据业务需求持续添加新图标
|
||||
|
||||
---
|
||||
|
||||
**维护者**:移动端开发团队
|
||||
**更新时间**:2025-07-01
|
||||
@@ -0,0 +1,280 @@
|
||||
# 移动端路由守卫使用指南
|
||||
|
||||
## 📋 概述
|
||||
|
||||
移动端路由守卫是一个全局的权限控制系统,确保用户未登录或登录失效时,访问需要权限的页面会自动跳转到登录页。
|
||||
|
||||
## 🎯 核心功能
|
||||
|
||||
### 1. 自动权限检查
|
||||
|
||||
- **页面加载时检查** → `onLoad()` 生命周期
|
||||
- **页面显示时检查** → `onShow()` 生命周期
|
||||
- **页面跳转前检查** → 使用守卫跳转方法
|
||||
- **HTTP 请求拦截** → 401 响应自动处理
|
||||
|
||||
### 2. 多层次验证
|
||||
|
||||
- ✅ 检查登录状态 `userStorage.isLoggedIn()`
|
||||
- ✅ 检查 Token 存在 `userStorage.getToken()`
|
||||
- ✅ 检查 Token 过期 `userState.checkTokenExpiration()`
|
||||
- ✅ 区分公开页面和受保护页面
|
||||
|
||||
### 3. 优雅的用户体验
|
||||
|
||||
- 🎨 友好的错误提示
|
||||
- 🎨 自动清理失效信息
|
||||
- 🎨 延迟跳转确保提示显示
|
||||
- 🎨 使用 `reLaunch` 清除页面栈
|
||||
|
||||
## 🔧 使用方法
|
||||
|
||||
### 在页面中使用
|
||||
|
||||
```javascript
|
||||
<script>
|
||||
import { checkPageAccess } from '@/utils/route-guard';
|
||||
|
||||
export default {
|
||||
onLoad() {
|
||||
// 页面加载时检查权限
|
||||
if (!checkPageAccess()) {
|
||||
return; // 权限检查失败,自动跳转登录页
|
||||
}
|
||||
|
||||
// 页面初始化逻辑
|
||||
this.initPageData();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 页面显示时再次检查权限(防止后台切换回来时失效)
|
||||
if (!checkPageAccess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 刷新页面数据
|
||||
this.refreshData();
|
||||
},
|
||||
|
||||
methods: {
|
||||
initPageData() {
|
||||
// 需要登录权限的初始化逻辑
|
||||
},
|
||||
|
||||
refreshData() {
|
||||
// 需要登录权限的数据刷新逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 带权限检查的页面跳转
|
||||
|
||||
```javascript
|
||||
<script>
|
||||
import { guardedNavigate, guardedSwitchTab } from '@/utils/route-guard';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
// 普通页面跳转
|
||||
goToProfile() {
|
||||
guardedNavigate('/pages/profile/index');
|
||||
},
|
||||
|
||||
// Tab页面切换
|
||||
switchToWorkbench() {
|
||||
guardedSwitchTab('/pages/workbench/index');
|
||||
},
|
||||
|
||||
// 带参数跳转
|
||||
goToDetail(id) {
|
||||
guardedNavigate(`/pages/detail/index?id=${id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 使用页面混入(可选)
|
||||
|
||||
```javascript
|
||||
<script>
|
||||
import { routeGuardMixin } from '@/utils/route-guard';
|
||||
|
||||
export default {
|
||||
// 使用混入,自动添加权限检查
|
||||
mixins: [routeGuardMixin],
|
||||
|
||||
onLoad() {
|
||||
// 混入会自动调用权限检查
|
||||
// 这里只需要写业务逻辑
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## ⚙️ 配置说明
|
||||
|
||||
### 公开页面配置
|
||||
|
||||
在 `mobile/src/utils/route-guard.js` 中配置无需登录的页面:
|
||||
|
||||
```javascript
|
||||
// 无需登录验证的页面列表
|
||||
const PUBLIC_PAGES = [
|
||||
'/pages/login/index', // 登录页
|
||||
'/pages/index/index', // 首页(功能受限但可访问)
|
||||
];
|
||||
```
|
||||
|
||||
### 自定义登录页路径
|
||||
|
||||
```javascript
|
||||
// 登录页路径
|
||||
const LOGIN_PAGE = '/pages/login/index';
|
||||
```
|
||||
|
||||
## 🧪 测试功能
|
||||
|
||||
访问测试页面验证路由守卫功能:
|
||||
|
||||
```
|
||||
页面路径: /pages/test-route/index
|
||||
```
|
||||
|
||||
测试页面提供:
|
||||
|
||||
- 📊 实时登录状态显示
|
||||
- 🔧 测试按钮(清除 Token、跳转等)
|
||||
- 📋 详细的操作日志
|
||||
- 🎯 权限检查结果验证
|
||||
|
||||
## 🔄 工作流程
|
||||
|
||||
### 权限检查流程
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[页面访问] --> B{是否公开页面?}
|
||||
B -->|是| C[允许访问]
|
||||
B -->|否| D{用户已登录?}
|
||||
D -->|否| E[跳转登录页]
|
||||
D -->|是| F{Token存在?}
|
||||
F -->|否| E
|
||||
F -->|是| G{Token未过期?}
|
||||
G -->|否| E
|
||||
G -->|是| C
|
||||
```
|
||||
|
||||
### HTTP 拦截流程
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[API请求] --> B[发送请求]
|
||||
B --> C{响应状态}
|
||||
C -->|200| D[正常处理]
|
||||
C -->|401| E[清除登录信息]
|
||||
E --> F[显示提示]
|
||||
F --> G[跳转登录页]
|
||||
```
|
||||
|
||||
## 📝 最佳实践
|
||||
|
||||
### 1. 页面权限设计原则
|
||||
|
||||
- **最小权限原则**:只有必要的页面才需要登录
|
||||
- **用户友好原则**:首页等入口页面可以部分访问
|
||||
- **安全优先原则**:敏感操作必须验证权限
|
||||
|
||||
### 2. 错误处理建议
|
||||
|
||||
```javascript
|
||||
onLoad() {
|
||||
try {
|
||||
if (!checkPageAccess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 业务逻辑
|
||||
await this.loadUserData();
|
||||
|
||||
} catch (error) {
|
||||
console.error('页面初始化失败:', error);
|
||||
uni.showToast({
|
||||
title: '页面加载失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 性能优化建议
|
||||
|
||||
- 权限检查放在业务逻辑之前
|
||||
- 避免在权限检查失败后继续执行后续逻辑
|
||||
- 合理使用页面缓存减少重复检查
|
||||
|
||||
## 🚨 注意事项
|
||||
|
||||
### 1. 公开页面处理
|
||||
|
||||
```javascript
|
||||
// ❌ 错误:在公开页面中强制检查权限
|
||||
onLoad() {
|
||||
if (!checkPageAccess()) return; // 首页不应该强制登录
|
||||
}
|
||||
|
||||
// ✅ 正确:区分功能的权限要求
|
||||
onLoad() {
|
||||
// 首页可以访问,但部分功能需要登录
|
||||
this.checkLoginForFeatures();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 避免循环跳转
|
||||
|
||||
```javascript
|
||||
// ❌ 错误:在登录页中调用权限检查
|
||||
// pages/login/index.vue
|
||||
onLoad() {
|
||||
checkPageAccess(); // 会导致循环跳转
|
||||
}
|
||||
|
||||
// ✅ 正确:登录页无需权限检查
|
||||
onLoad() {
|
||||
// 直接执行登录页逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Token 刷新处理
|
||||
|
||||
```javascript
|
||||
// 建议在HTTP拦截器中处理Token刷新
|
||||
// 避免在页面级别处理复杂的Token逻辑
|
||||
```
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
- [移动端用户管理工具](./移动端用户管理工具使用指南.md)
|
||||
- [移动端 API 请求规范](./移动端API请求规范.md)
|
||||
- [移动端开发规范总结](./移动端开发规范总结.md)
|
||||
|
||||
## 💡 常见问题
|
||||
|
||||
### Q: 如何添加新的公开页面?
|
||||
|
||||
A: 在 `route-guard.js` 的 `PUBLIC_PAGES` 数组中添加页面路径。
|
||||
|
||||
### Q: 权限检查失败但没有跳转到登录页?
|
||||
|
||||
A: 检查控制台日志,确认 `reLaunch` 是否执行成功,必要时检查页面路径配置。
|
||||
|
||||
### Q: 如何自定义权限检查逻辑?
|
||||
|
||||
A: 可以扩展 `checkPageAccess` 函数,添加更复杂的权限验证逻辑。
|
||||
|
||||
### Q: 在什么情况下需要使用 `routeGuardMixin`?
|
||||
|
||||
A: 当页面逻辑复杂,希望自动化权限检查时使用,但通常建议手动调用以保持代码清晰。
|
||||
+9
-4
@@ -1,15 +1,20 @@
|
||||
<script>
|
||||
import { initRouteGuard } from '@/utils/route-guard';
|
||||
|
||||
export default {
|
||||
onLaunch: function () {
|
||||
console.log('App Launch')
|
||||
console.log('App Launch');
|
||||
|
||||
// 初始化全局路由守卫
|
||||
initRouteGuard();
|
||||
},
|
||||
onShow: function () {
|
||||
console.log('App Show')
|
||||
console.log('App Show');
|
||||
},
|
||||
onHide: function () {
|
||||
console.log('App Hide')
|
||||
console.log('App Hide');
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
+28
-7
@@ -1,11 +1,32 @@
|
||||
import { createSSRApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import uviewPlus from 'uview-plus'
|
||||
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';
|
||||
|
||||
// 预加载字体文件 - 使用阿里CDN
|
||||
uni.loadFontFace({
|
||||
family: 'uicon-iconfont',
|
||||
source: 'url("https://at.alicdn.com/t/font_2225171_8kdcwk4po24.ttf")',
|
||||
success() {
|
||||
console.log('uview-plus图标字体加载成功');
|
||||
},
|
||||
fail(error) {
|
||||
console.error('uview-plus图标字体加载失败', error);
|
||||
},
|
||||
});
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
app.use(uviewPlus)
|
||||
const app = createSSRApp(App);
|
||||
|
||||
// 注册 uview-plus
|
||||
app.use(uviewPlus);
|
||||
|
||||
return {
|
||||
app
|
||||
}
|
||||
app,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,22 @@
|
||||
{
|
||||
"easycom": {
|
||||
"autoscan": true,
|
||||
"custom": {
|
||||
"^u-(.*)": "uview-plus/components/u-$1/u-$1.vue",
|
||||
"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
|
||||
"^uv-(.*)": "uview-plus/components/u-$1/u-$1.vue"
|
||||
}
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录",
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarBackgroundColor": "transparent",
|
||||
"backgroundColor": "transparent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
@@ -20,6 +37,21 @@
|
||||
"navigationBarTitleText": "我的",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/iconfont-demo/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "图标演示",
|
||||
"navigationBarBackgroundColor": "#5d87ff",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/test-route/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "路由守卫测试",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<view class="iconfont-demo">
|
||||
<view class="demo-header">
|
||||
<text class="demo-title">iconfont 图标库演示</text>
|
||||
<text class="demo-subtitle">基于iconfont.cn的图标字体</text>
|
||||
</view>
|
||||
|
||||
<view class="demo-content">
|
||||
<!-- 基础图标 -->
|
||||
<view class="icon-section">
|
||||
<text class="section-title">基础图标</text>
|
||||
<view class="icon-grid">
|
||||
<view class="icon-item">
|
||||
<text class="iconfont icon-user"></text>
|
||||
<text class="icon-name">用户</text>
|
||||
<text class="icon-code">icon-user</text>
|
||||
</view>
|
||||
<view class="icon-item">
|
||||
<text class="iconfont icon-lock"></text>
|
||||
<text class="icon-name">锁定</text>
|
||||
<text class="icon-code">icon-lock</text>
|
||||
</view>
|
||||
<view class="icon-item">
|
||||
<text class="iconfont icon-eye"></text>
|
||||
<text class="icon-name">眼睛</text>
|
||||
<text class="icon-code">icon-eye</text>
|
||||
</view>
|
||||
<view class="icon-item">
|
||||
<text class="iconfont icon-eye-close"></text>
|
||||
<text class="icon-name">关闭眼睛</text>
|
||||
<text class="icon-code">icon-eye-close</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作图标 -->
|
||||
<view class="icon-section">
|
||||
<text class="section-title">操作图标</text>
|
||||
<view class="icon-grid">
|
||||
<view class="icon-item">
|
||||
<text class="iconfont icon-login"></text>
|
||||
<text class="icon-name">登录</text>
|
||||
<text class="icon-code">icon-login</text>
|
||||
</view>
|
||||
<view class="icon-item">
|
||||
<text class="iconfont icon-search"></text>
|
||||
<text class="icon-name">搜索</text>
|
||||
<text class="icon-code">icon-search</text>
|
||||
</view>
|
||||
<view class="icon-item">
|
||||
<text class="iconfont icon-add"></text>
|
||||
<text class="icon-name">添加</text>
|
||||
<text class="icon-code">icon-add</text>
|
||||
</view>
|
||||
<view class="icon-item">
|
||||
<text class="iconfont icon-close"></text>
|
||||
<text class="icon-name">关闭</text>
|
||||
<text class="icon-code">icon-close</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 导航图标 -->
|
||||
<view class="icon-section">
|
||||
<text class="section-title">导航图标</text>
|
||||
<view class="icon-grid">
|
||||
<view class="icon-item">
|
||||
<text class="iconfont icon-home"></text>
|
||||
<text class="icon-name">首页</text>
|
||||
<text class="icon-code">icon-home</text>
|
||||
</view>
|
||||
<view class="icon-item">
|
||||
<text class="iconfont icon-message"></text>
|
||||
<text class="icon-name">消息</text>
|
||||
<text class="icon-code">icon-message</text>
|
||||
</view>
|
||||
<view class="icon-item">
|
||||
<text class="iconfont icon-setting"></text>
|
||||
<text class="icon-name">设置</text>
|
||||
<text class="icon-code">icon-setting</text>
|
||||
</view>
|
||||
<view class="icon-item">
|
||||
<text class="iconfont icon-arrow-right"></text>
|
||||
<text class="icon-name">箭头右</text>
|
||||
<text class="icon-code">icon-arrow-right</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 联系图标 -->
|
||||
<view class="icon-section">
|
||||
<text class="section-title">联系图标</text>
|
||||
<view class="icon-grid">
|
||||
<view class="icon-item">
|
||||
<text class="iconfont icon-phone"></text>
|
||||
<text class="icon-name">电话</text>
|
||||
<text class="icon-code">icon-phone</text>
|
||||
</view>
|
||||
<view class="icon-item">
|
||||
<text class="iconfont icon-mail"></text>
|
||||
<text class="icon-name">邮箱</text>
|
||||
<text class="icon-code">icon-mail</text>
|
||||
</view>
|
||||
<view class="icon-item">
|
||||
<text class="iconfont icon-check"></text>
|
||||
<text class="icon-name">确认</text>
|
||||
<text class="icon-code">icon-check</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 使用说明 -->
|
||||
<view class="usage-section">
|
||||
<text class="section-title">使用方法</text>
|
||||
<view class="usage-code">
|
||||
<text class="code-line">
|
||||
<text class="iconfont icon-user"></text>
|
||||
</text>
|
||||
<text class="code-line">
|
||||
<text class="iconfont icon-lock"></text>
|
||||
</text>
|
||||
<text class="code-line">/* 自定义样式 */</text>
|
||||
<text class="code-line">.custom-icon {</text>
|
||||
<text class="code-line">font-size: 24px;</text>
|
||||
<text class="code-line">color: #5d87ff;</text>
|
||||
<text class="code-line">}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'IconFontDemo',
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.iconfont-demo {
|
||||
min-height: 100vh;
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #222;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.demo-subtitle {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.icon-section {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #5d87ff;
|
||||
}
|
||||
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.icon-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
background: #fafbfc;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.icon-item:hover {
|
||||
border-color: #5d87ff;
|
||||
background: #f0f4ff;
|
||||
}
|
||||
|
||||
.icon-item .iconfont {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-bottom: 8px;
|
||||
filter: brightness(0) saturate(100%) invert(38%) sepia(87%) saturate(6400%) hue-rotate(225deg)
|
||||
brightness(104%) contrast(104%);
|
||||
}
|
||||
|
||||
.icon-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.icon-code {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
background: #f1f3f4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.usage-section {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.usage-code {
|
||||
background: #2d3748;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.code-line {
|
||||
display: block;
|
||||
color: #e2e8f0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.code-line:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -166,6 +166,9 @@ import Card from '@/components/Card.vue';
|
||||
import ServiceItem from '@/components/ServiceItem.vue';
|
||||
import StatCard from '@/components/StatCard.vue';
|
||||
import GridLayout from '@/components/GridLayout.vue';
|
||||
import { userStorage } from '@/utils/request';
|
||||
import { userManager, userState } from '@/utils/user';
|
||||
import { guardedSwitchTab } from '@/utils/route-guard';
|
||||
|
||||
export default {
|
||||
name: 'HomePage',
|
||||
@@ -179,7 +182,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
userInfo: {
|
||||
name: '张同学',
|
||||
name: '用户',
|
||||
},
|
||||
currentDateTime: '',
|
||||
weather: {
|
||||
@@ -279,6 +282,7 @@ export default {
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.checkLoginStatus();
|
||||
this.updateDateTime();
|
||||
this.startTimeUpdate();
|
||||
},
|
||||
@@ -288,6 +292,26 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
// 使用新的状态检查工具
|
||||
if (!userState.autoLoginCheck()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取并更新用户信息
|
||||
this.loadUserInfo();
|
||||
},
|
||||
|
||||
// 加载用户信息
|
||||
loadUserInfo() {
|
||||
const displayUser = userManager.formatUserForDisplay();
|
||||
if (displayUser) {
|
||||
this.userInfo.name = displayUser.name;
|
||||
this.userInfo.avatar = displayUser.avatar;
|
||||
}
|
||||
},
|
||||
|
||||
updateDateTime() {
|
||||
const now = new Date();
|
||||
const month = now.getMonth() + 1;
|
||||
@@ -327,7 +351,7 @@ export default {
|
||||
uni.showToast({ title: `使用${tool.title}`, icon: 'none' });
|
||||
},
|
||||
navigateToService() {
|
||||
uni.switchTab({ url: '/pages/workbench/index' });
|
||||
guardedSwitchTab('/pages/workbench/index');
|
||||
},
|
||||
viewAllTodos() {
|
||||
uni.showToast({ title: '查看全部待办', icon: 'none' });
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -155,6 +155,9 @@ import NavBar from '@/components/NavBar.vue';
|
||||
import Card from '@/components/Card.vue';
|
||||
import StatCard from '@/components/StatCard.vue';
|
||||
import GridLayout from '@/components/GridLayout.vue';
|
||||
import { userStorage } from '@/utils/request';
|
||||
import { userManager, userState } from '@/utils/user';
|
||||
import { checkPageAccess } from '@/utils/route-guard';
|
||||
|
||||
export default {
|
||||
name: 'ProfilePage',
|
||||
@@ -167,13 +170,13 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
userInfo: {
|
||||
name: '张同学',
|
||||
major: '计算机科学与技术',
|
||||
grade: '大三',
|
||||
studentId: '2021001234',
|
||||
gpa: 4.2,
|
||||
credits: 95,
|
||||
ranking: 15,
|
||||
name: '用户',
|
||||
major: '未设置',
|
||||
grade: '未设置',
|
||||
studentId: '未设置',
|
||||
gpa: 0,
|
||||
credits: 0,
|
||||
ranking: 0,
|
||||
},
|
||||
learningStats: [
|
||||
{ number: 5, label: '本周课程' },
|
||||
@@ -288,6 +291,24 @@ export default {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
return userState.autoLoginCheck();
|
||||
},
|
||||
|
||||
// 加载用户信息
|
||||
loadUserInfo() {
|
||||
const displayUser = userManager.formatUserForDisplay();
|
||||
if (displayUser) {
|
||||
this.userInfo.name = displayUser.name;
|
||||
this.userInfo.avatar = displayUser.avatar;
|
||||
this.userInfo.department = displayUser.department;
|
||||
this.userInfo.phone = displayUser.phone;
|
||||
this.userInfo.email = displayUser.email;
|
||||
this.userInfo.studentId = userManager.getCurrentUser()?.userId || '未设置';
|
||||
}
|
||||
},
|
||||
|
||||
handleSettings() {
|
||||
uni.showToast({ title: '设置', icon: 'none' });
|
||||
},
|
||||
@@ -344,17 +365,50 @@ export default {
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({ title: '正在退出...', icon: 'loading' });
|
||||
uni.showLoading({ title: '正在退出...' });
|
||||
|
||||
// 清除登录信息
|
||||
userStorage.clearLoginInfo();
|
||||
|
||||
setTimeout(() => {
|
||||
uni.showToast({ title: '已退出登录', icon: 'success' });
|
||||
// 这里可以跳转到登录页面
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '已退出登录',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 跳转到登录页面
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index'
|
||||
});
|
||||
}, 500);
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
onLoad() {},
|
||||
onLoad() {
|
||||
// 检查页面访问权限
|
||||
if (!checkPageAccess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.checkLoginStatus()) {
|
||||
this.loadUserInfo();
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 页面显示时再次检查权限
|
||||
if (!checkPageAccess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 刷新用户信息
|
||||
this.loadUserInfo();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<view class="test-page">
|
||||
<NavBar title="路由守卫测试" />
|
||||
|
||||
<view class="content">
|
||||
<view class="test-section">
|
||||
<text class="section-title">🛡️ 路由守卫测试页面</text>
|
||||
<text class="section-desc">如果你能看到这个页面,说明已经通过了登录验证</text>
|
||||
</view>
|
||||
|
||||
<view class="test-info">
|
||||
<view class="info-item">
|
||||
<text class="info-label">当前时间:</text>
|
||||
<text class="info-value">{{ currentTime }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">登录状态:</text>
|
||||
<text class="info-value" :class="isLoggedIn ? 'status-ok' : 'status-error'">
|
||||
{{ isLoggedIn ? '✅ 已登录' : '❌ 未登录' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">Token状态:</text>
|
||||
<text class="info-value" :class="hasToken ? 'status-ok' : 'status-error'">
|
||||
{{ hasToken ? '✅ 有效' : '❌ 无效' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">用户信息:</text>
|
||||
<text class="info-value">{{ userName || '未获取' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="test-buttons">
|
||||
<button class="test-btn" @click="testRedirectToLogin">测试跳转登录页</button>
|
||||
<button class="test-btn" @click="testClearToken">清除Token测试</button>
|
||||
<button class="test-btn" @click="testNavigateWithGuard">带守卫的页面跳转</button>
|
||||
<button class="test-btn" @click="refreshPageInfo">刷新页面信息</button>
|
||||
</view>
|
||||
|
||||
<view class="log-section">
|
||||
<text class="section-title">📋 测试日志</text>
|
||||
<scroll-view class="log-content" scroll-y>
|
||||
<view
|
||||
v-for="(log, index) in logs"
|
||||
:key="index"
|
||||
class="log-item"
|
||||
:class="log.type"
|
||||
>
|
||||
<text class="log-time">{{ log.time }}</text>
|
||||
<text class="log-text">{{ log.message }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavBar from '@/components/NavBar.vue';
|
||||
import { userStorage } from '@/utils/request';
|
||||
import { userManager, userState } from '@/utils/user';
|
||||
import { checkPageAccess, guardedNavigate } from '@/utils/route-guard';
|
||||
|
||||
export default {
|
||||
name: 'TestRoutePage',
|
||||
components: {
|
||||
NavBar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentTime: '',
|
||||
isLoggedIn: false,
|
||||
hasToken: false,
|
||||
userName: '',
|
||||
logs: [],
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.addLog('info', '页面加载开始');
|
||||
|
||||
// 检查页面访问权限
|
||||
if (!checkPageAccess()) {
|
||||
this.addLog('error', '页面访问权限检查失败,应该会自动跳转到登录页');
|
||||
return;
|
||||
}
|
||||
|
||||
this.addLog('success', '页面访问权限检查通过');
|
||||
this.refreshPageInfo();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.addLog('info', '页面显示事件触发');
|
||||
|
||||
// 页面显示时再次检查权限
|
||||
if (!checkPageAccess()) {
|
||||
this.addLog('error', '页面显示时权限检查失败');
|
||||
return;
|
||||
}
|
||||
|
||||
this.addLog('success', '页面显示时权限检查通过');
|
||||
this.updateCurrentTime();
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
this.addLog('info', '页面卸载');
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 刷新页面信息
|
||||
refreshPageInfo() {
|
||||
this.updateCurrentTime();
|
||||
this.checkLoginStatus();
|
||||
this.checkTokenStatus();
|
||||
this.loadUserInfo();
|
||||
this.addLog('info', '页面信息已刷新');
|
||||
},
|
||||
|
||||
// 更新当前时间
|
||||
updateCurrentTime() {
|
||||
this.currentTime = new Date().toLocaleString();
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
this.isLoggedIn = userStorage.isLoggedIn();
|
||||
this.addLog('info', `登录状态检查: ${this.isLoggedIn ? '已登录' : '未登录'}`);
|
||||
},
|
||||
|
||||
// 检查Token状态
|
||||
checkTokenStatus() {
|
||||
const token = userStorage.getToken();
|
||||
this.hasToken = !!token;
|
||||
const isExpired = userState.checkTokenExpiration();
|
||||
|
||||
this.addLog('info', `Token检查: ${this.hasToken ? '存在' : '不存在'}`);
|
||||
if (this.hasToken) {
|
||||
this.addLog('info', `Token过期检查: ${isExpired ? '已过期' : '未过期'}`);
|
||||
}
|
||||
},
|
||||
|
||||
// 加载用户信息
|
||||
loadUserInfo() {
|
||||
const user = userManager.getCurrentUser();
|
||||
this.userName = user ? user.nickName || user.userName : '';
|
||||
this.addLog('info', `用户信息: ${this.userName || '未获取到'}`);
|
||||
},
|
||||
|
||||
// 测试跳转到登录页
|
||||
testRedirectToLogin() {
|
||||
this.addLog('warning', '手动跳转到登录页测试');
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index',
|
||||
});
|
||||
},
|
||||
|
||||
// 测试清除Token
|
||||
testClearToken() {
|
||||
this.addLog('warning', '清除Token进行测试');
|
||||
userStorage.clearLoginInfo();
|
||||
this.addLog('error', 'Token已清除,页面应该会自动跳转到登录页');
|
||||
|
||||
// 模拟页面重新检查权限
|
||||
setTimeout(() => {
|
||||
if (!checkPageAccess()) {
|
||||
this.addLog('success', '权限检查失败,正在跳转登录页');
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
// 测试带守卫的页面跳转
|
||||
testNavigateWithGuard() {
|
||||
this.addLog('info', '测试带权限检查的页面跳转');
|
||||
const result = guardedNavigate('/pages/profile/index');
|
||||
|
||||
if (result) {
|
||||
this.addLog('success', '跳转成功');
|
||||
} else {
|
||||
this.addLog('error', '跳转失败或被拦截');
|
||||
}
|
||||
},
|
||||
|
||||
// 添加日志
|
||||
addLog(type, message) {
|
||||
const log = {
|
||||
type,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
message,
|
||||
};
|
||||
|
||||
this.logs.unshift(log);
|
||||
|
||||
// 限制日志数量
|
||||
if (this.logs.length > 20) {
|
||||
this.logs.pop();
|
||||
}
|
||||
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.test-page {
|
||||
min-height: 100vh;
|
||||
background-color: $uni-bg-color-grey;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
background: $uni-bg-color;
|
||||
border-radius: $uni-border-radius-lg;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: $uni-text-color;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
font-size: 14px;
|
||||
color: $uni-text-color-grey;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.test-info {
|
||||
background: $uni-bg-color;
|
||||
border-radius: $uni-border-radius-lg;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 14px;
|
||||
color: $uni-text-color-grey;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: $uni-text-color;
|
||||
font-weight: 500;
|
||||
|
||||
&.status-ok {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
&.status-error {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.test-buttons {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.test-btn {
|
||||
width: 100%;
|
||||
background: $uni-color-primary;
|
||||
color: $uni-text-color-inverse;
|
||||
border: none;
|
||||
border-radius: $uni-border-radius-base;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $uni-color-primary-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.log-section {
|
||||
background: $uni-bg-color;
|
||||
border-radius: $uni-border-radius-lg;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
max-height: 300px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: $uni-border-radius-base;
|
||||
border-left: 3px solid;
|
||||
|
||||
&.info {
|
||||
background: #f0f9ff;
|
||||
border-left-color: #0ea5e9;
|
||||
}
|
||||
|
||||
&.success {
|
||||
background: #f0f9f4;
|
||||
border-left-color: #22c55e;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: #fffbeb;
|
||||
border-left-color: #f59e0b;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: #fef2f2;
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-size: 12px;
|
||||
color: $uni-text-color-grey;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.log-text {
|
||||
font-size: 13px;
|
||||
color: $uni-text-color;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
@@ -165,6 +165,8 @@ import NavBar from '@/components/NavBar.vue';
|
||||
import Card from '@/components/Card.vue';
|
||||
import ServiceItem from '@/components/ServiceItem.vue';
|
||||
import GridLayout from '@/components/GridLayout.vue';
|
||||
import { userStorage } from '@/utils/request';
|
||||
import { checkPageAccess } from '@/utils/route-guard';
|
||||
|
||||
export default {
|
||||
name: 'WorkbenchPage',
|
||||
@@ -340,8 +342,31 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
if (!userStorage.isLoggedIn()) {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index',
|
||||
});
|
||||
return;
|
||||
}
|
||||
},
|
||||
},
|
||||
onLoad() {
|
||||
// 检查页面访问权限
|
||||
if (!checkPageAccess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.checkLoginStatus();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 页面显示时再次检查权限
|
||||
if (!checkPageAccess()) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
onLoad() {},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1 +1,75 @@
|
||||
/* 空的iconfont文件 - 用于解决依赖问题 */
|
||||
.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");
|
||||
}
|
||||
|
||||
/* 眼睛图标(显示密码) */
|
||||
.icon-eye {
|
||||
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 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 眼睛关闭图标(隐藏密码) */
|
||||
.icon-eye-close {
|
||||
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 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 登录图标 */
|
||||
.icon-login {
|
||||
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='M11 7L9.6 8.4l2.6 2.6H2v2h10.2l-2.6 2.6L11 17l5-5-5-5zm9 12h-8v2h8c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-8v2h8v14z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 箭头右图标 */
|
||||
.icon-arrow-right {
|
||||
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='M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 更多常用图标 */
|
||||
.icon-home {
|
||||
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='M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.icon-message {
|
||||
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='M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.icon-setting {
|
||||
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='M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.icon-search {
|
||||
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='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.icon-add {
|
||||
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='M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.icon-check {
|
||||
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='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.icon-close {
|
||||
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='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.icon-phone {
|
||||
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='M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.icon-mail {
|
||||
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='M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z'/%3E%3C/svg%3E");
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 276 KiB |
@@ -15,6 +15,12 @@
|
||||
// 引入 uview-plus 样式
|
||||
@import 'uview-plus/theme.scss';
|
||||
|
||||
// 引入 uview-plus 图标字体 - 使用阿里CDN
|
||||
@font-face {
|
||||
font-family: 'uicon-iconfont';
|
||||
src: url('https://at.alicdn.com/t/font_2225171_8kdcwk4po24.ttf') format('truetype');
|
||||
}
|
||||
|
||||
/* 原有的Sass变量定义 - 保持向后兼容 */
|
||||
$uni-color-primary: rgb(93, 135, 255);
|
||||
$uni-color-primary-dark: rgb(73, 115, 235);
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// @ts-nocheck
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
// 配置信息
|
||||
const ENCRYPT_CONFIG = {
|
||||
// RSA公钥 - 与后端保持一致 (更新为正确版本,包含ijT7h)
|
||||
RSA_PUBLIC_KEY:
|
||||
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
|
||||
CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
|
||||
TENANT_ID: '000000',
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机AES密钥
|
||||
* @param {number} length 密钥长度,默认32位
|
||||
* @returns {string} AES密钥
|
||||
*/
|
||||
export function generateRandomAESKey(length = 32) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* AES加密数据
|
||||
* @param {string} data 待加密的数据
|
||||
* @param {string} key AES密钥
|
||||
* @returns {string} 加密后的数据
|
||||
*/
|
||||
export function encryptWithAES(data, key) {
|
||||
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
|
||||
const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return encrypted.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA加密AES密钥 - 浏览器兼容版本
|
||||
* @param {string} aesKey AES密钥
|
||||
* @returns {Promise<string|null>} RSA加密后的AES密钥
|
||||
*/
|
||||
export async function encryptAESKeyWithRSA(aesKey) {
|
||||
try {
|
||||
// 将Base64公钥转换为ArrayBuffer
|
||||
const publicKeyData = Uint8Array.from(atob(ENCRYPT_CONFIG.RSA_PUBLIC_KEY), (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
|
||||
// 导入RSA公钥
|
||||
const publicKey = await crypto.subtle.importKey(
|
||||
'spki',
|
||||
publicKeyData,
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
hash: 'SHA-1',
|
||||
},
|
||||
false,
|
||||
['encrypt'],
|
||||
);
|
||||
|
||||
// 将AES密钥转为Base64
|
||||
const aesKeyBase64 = btoa(aesKey);
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(aesKeyBase64);
|
||||
|
||||
// RSA加密
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
},
|
||||
publicKey,
|
||||
data,
|
||||
);
|
||||
|
||||
// 转换为Base64
|
||||
const encryptedArray = new Uint8Array(encrypted);
|
||||
return btoa(String.fromCharCode.apply(null, encryptedArray));
|
||||
} catch (error) {
|
||||
console.error('RSA加密失败:', error.message);
|
||||
console.log('🔄 回退到简化加密方案...');
|
||||
|
||||
// 回退方案:使用AES密钥的Base64编码(临时方案)
|
||||
try {
|
||||
return btoa(aesKey);
|
||||
} catch (fallbackError) {
|
||||
console.error('回退加密也失败:', fallbackError.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录数据加密处理
|
||||
* @param {Object} loginData 登录数据对象
|
||||
* @returns {Promise<Object>} 包含加密数据和请求头的对象
|
||||
*/
|
||||
export async function encryptLoginData(loginData) {
|
||||
try {
|
||||
// 1. 生成AES密钥
|
||||
const aesKey = generateRandomAESKey(32);
|
||||
console.log('🔑 生成AES密钥:', aesKey);
|
||||
|
||||
// 2. 准备完整的登录数据
|
||||
const fullLoginData = {
|
||||
tenantId: loginData.tenantId || ENCRYPT_CONFIG.TENANT_ID,
|
||||
username: loginData.username,
|
||||
password: loginData.password,
|
||||
clientId: loginData.clientId || ENCRYPT_CONFIG.CLIENT_ID,
|
||||
grantType: loginData.grantType || 'password',
|
||||
};
|
||||
|
||||
console.log('📋 登录数据:', JSON.stringify(fullLoginData, null, 2));
|
||||
|
||||
// 3. AES加密登录数据
|
||||
const encryptedData = encryptWithAES(JSON.stringify(fullLoginData), aesKey);
|
||||
console.log('🔐 AES加密后数据长度:', encryptedData.length);
|
||||
|
||||
// 4. RSA加密AES密钥
|
||||
const encryptedKey = await encryptAESKeyWithRSA(aesKey);
|
||||
if (!encryptedKey) {
|
||||
throw new Error('RSA加密AES密钥失败');
|
||||
}
|
||||
console.log('🔑 RSA加密后密钥长度:', encryptedKey.length);
|
||||
|
||||
// 5. 返回加密结果和请求头
|
||||
return {
|
||||
success: true,
|
||||
encryptedData: encryptedData,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
clientid: ENCRYPT_CONFIG.CLIENT_ID,
|
||||
'encrypt-key': encryptedKey,
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
isToken: 'false',
|
||||
isEncrypt: 'true',
|
||||
repeatSubmit: 'false',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('登录数据加密失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加密配置
|
||||
*/
|
||||
export function getEncryptConfig() {
|
||||
return ENCRYPT_CONFIG;
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// @ts-nocheck
|
||||
import CryptoJS from 'crypto-js';
|
||||
import JSEncrypt from 'jsencrypt';
|
||||
|
||||
// 配置信息
|
||||
const ENCRYPT_CONFIG = {
|
||||
// RSA公钥 - 与后端保持一致 (更新为正确版本,包含ijT7h)
|
||||
RSA_PUBLIC_KEY:
|
||||
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
|
||||
CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
|
||||
TENANT_ID: '000000',
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机AES密钥
|
||||
* @param {number} length 密钥长度,默认32位
|
||||
* @returns {string} AES密钥
|
||||
*/
|
||||
export function generateRandomAESKey(length = 32) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* AES加密数据
|
||||
* @param {string} data 待加密的数据
|
||||
* @param {string} key AES密钥
|
||||
* @returns {string} 加密后的数据
|
||||
*/
|
||||
export function encryptWithAES(data, key) {
|
||||
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
|
||||
const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return encrypted.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA加密AES密钥 - 使用JSEncrypt
|
||||
* @param {string} aesKey AES密钥
|
||||
* @returns {string|null} RSA加密后的AES密钥
|
||||
*/
|
||||
export function encryptAESKeyWithRSA(aesKey) {
|
||||
try {
|
||||
console.log('🔧 开始RSA加密,AES密钥:', aesKey);
|
||||
|
||||
// 构造完整的PEM格式RSA公钥
|
||||
const pemKey = `-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqEfJl9KxCi6s3Fpjs234+uCvG
|
||||
SIDnR585F0jlTmWxbv0yYWe6xQDfnmhpJdwuG7nHEHfxIxLI/Yq+4fejDAMCAwEA
|
||||
AQ==
|
||||
-----END PUBLIC KEY-----`;
|
||||
|
||||
console.log('🔧 RSA公钥PEM格式已构造');
|
||||
|
||||
// 创建JSEncrypt实例
|
||||
const encrypt = new JSEncrypt();
|
||||
|
||||
// 设置公钥
|
||||
const keySetResult = encrypt.setPublicKey(pemKey);
|
||||
console.log('🔧 公钥设置结果:', keySetResult);
|
||||
|
||||
// 将AES密钥转为Base64(与custom-login-script.js保持一致)
|
||||
const aesKeyBase64 = btoa(unescape(encodeURIComponent(aesKey)));
|
||||
console.log('🔧 AES密钥Base64:', aesKeyBase64);
|
||||
|
||||
// RSA加密
|
||||
const encrypted = encrypt.encrypt(aesKeyBase64);
|
||||
|
||||
console.log('🔧 RSA加密结果:', encrypted);
|
||||
|
||||
if (!encrypted) {
|
||||
throw new Error('JSEncrypt RSA加密返回null,可能是公钥格式错误或加密失败');
|
||||
}
|
||||
|
||||
console.log('✅ RSA加密成功,结果长度:', encrypted.length);
|
||||
return encrypted;
|
||||
} catch (error) {
|
||||
console.error('❌ RSA加密失败:', error.message);
|
||||
console.error('❌ 错误堆栈:', error.stack);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录数据加密处理
|
||||
* @param {Object} loginData 登录数据对象
|
||||
* @returns {Object} 包含加密数据和请求头的对象
|
||||
*/
|
||||
export function encryptLoginData(loginData) {
|
||||
try {
|
||||
// 1. 生成AES密钥
|
||||
const aesKey = generateRandomAESKey(32);
|
||||
console.log('🔑 生成AES密钥:', aesKey);
|
||||
|
||||
// 2. 准备完整的登录数据
|
||||
const fullLoginData = {
|
||||
tenantId: loginData.tenantId || ENCRYPT_CONFIG.TENANT_ID,
|
||||
username: loginData.username,
|
||||
password: loginData.password,
|
||||
clientId: loginData.clientId || ENCRYPT_CONFIG.CLIENT_ID,
|
||||
grantType: loginData.grantType || 'password',
|
||||
};
|
||||
|
||||
console.log('📋 登录数据:', JSON.stringify(fullLoginData, null, 2));
|
||||
|
||||
// 3. AES加密登录数据
|
||||
const encryptedData = encryptWithAES(JSON.stringify(fullLoginData), aesKey);
|
||||
console.log('🔐 AES加密后数据长度:', encryptedData.length);
|
||||
|
||||
// 4. RSA加密AES密钥
|
||||
const encryptedKey = encryptAESKeyWithRSA(aesKey);
|
||||
if (!encryptedKey) {
|
||||
throw new Error('RSA加密AES密钥失败');
|
||||
}
|
||||
console.log('🔑 RSA加密后密钥长度:', encryptedKey.length);
|
||||
|
||||
// 5. 返回加密结果和请求头
|
||||
return {
|
||||
success: true,
|
||||
encryptedData: encryptedData,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
clientid: ENCRYPT_CONFIG.CLIENT_ID,
|
||||
'encrypt-key': encryptedKey,
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
isToken: 'false',
|
||||
isEncrypt: 'true',
|
||||
repeatSubmit: 'false',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('登录数据加密失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加密配置
|
||||
*/
|
||||
export function getEncryptConfig() {
|
||||
return ENCRYPT_CONFIG;
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
// @ts-nocheck
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
// 配置信息
|
||||
const ENCRYPT_CONFIG = {
|
||||
// RSA公钥 - 与后端保持一致 (更新为正确版本,包含ijT7h)
|
||||
RSA_PUBLIC_KEY:
|
||||
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
|
||||
CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
|
||||
TENANT_ID: '000000',
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机AES密钥
|
||||
* @param {number} length 密钥长度,默认32位
|
||||
* @returns {string} AES密钥
|
||||
*/
|
||||
export function generateRandomAESKey(length = 32) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* AES加密数据
|
||||
* @param {string} data 待加密的数据
|
||||
* @param {string} key AES密钥
|
||||
* @returns {string} 加密后的数据
|
||||
*/
|
||||
export function encryptWithAES(data, key) {
|
||||
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
|
||||
const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return encrypted.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64字符串转ArrayBuffer
|
||||
* @param {string} base64
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
function base64ToArrayBuffer(base64) {
|
||||
const binaryString = atob(base64);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* ArrayBuffer转Base64字符串
|
||||
* @param {ArrayBuffer} buffer
|
||||
* @returns {string}
|
||||
*/
|
||||
function arrayBufferToBase64(buffer) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入RSA公钥
|
||||
* @param {string} publicKeyBase64 Base64格式的公钥
|
||||
* @returns {Promise<CryptoKey>}
|
||||
*/
|
||||
async function importRSAPublicKey(publicKeyBase64) {
|
||||
try {
|
||||
const keyBuffer = base64ToArrayBuffer(publicKeyBase64);
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'spki',
|
||||
keyBuffer,
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
hash: 'SHA-1',
|
||||
},
|
||||
false,
|
||||
['encrypt'],
|
||||
);
|
||||
|
||||
return key;
|
||||
} catch (error) {
|
||||
console.error('导入RSA公钥失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA加密AES密钥 - 使用Web Crypto API
|
||||
* @param {string} aesKey AES密钥
|
||||
* @returns {Promise<string|null>} RSA加密后的AES密钥
|
||||
*/
|
||||
export async function encryptAESKeyWithRSA(aesKey) {
|
||||
try {
|
||||
// 检查Web Crypto API支持
|
||||
if (!crypto || !crypto.subtle) {
|
||||
throw new Error('当前环境不支持Web Crypto API');
|
||||
}
|
||||
|
||||
// 导入RSA公钥
|
||||
const publicKey = await importRSAPublicKey(ENCRYPT_CONFIG.RSA_PUBLIC_KEY);
|
||||
|
||||
// 将AES密钥转为Base64(与custom-login-script.js保持一致)
|
||||
const aesKeyBase64 = btoa(unescape(encodeURIComponent(aesKey)));
|
||||
|
||||
// 将字符串转为ArrayBuffer
|
||||
const dataBuffer = new TextEncoder().encode(aesKeyBase64);
|
||||
|
||||
// RSA加密
|
||||
const encryptedBuffer = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
},
|
||||
publicKey,
|
||||
dataBuffer,
|
||||
);
|
||||
|
||||
// 转为Base64字符串
|
||||
const encryptedBase64 = arrayBufferToBase64(encryptedBuffer);
|
||||
|
||||
console.log('✅ Web Crypto RSA加密成功,结果长度:', encryptedBase64.length);
|
||||
return encryptedBase64;
|
||||
} catch (error) {
|
||||
console.error('Web Crypto RSA加密失败:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录数据加密处理
|
||||
* @param {Object} loginData 登录数据对象
|
||||
* @returns {Promise<Object>} 包含加密数据和请求头的对象
|
||||
*/
|
||||
export async function encryptLoginData(loginData) {
|
||||
try {
|
||||
// 1. 生成AES密钥
|
||||
const aesKey = generateRandomAESKey(32);
|
||||
console.log('🔑 生成AES密钥:', aesKey);
|
||||
|
||||
// 2. 准备完整的登录数据
|
||||
const fullLoginData = {
|
||||
tenantId: loginData.tenantId || ENCRYPT_CONFIG.TENANT_ID,
|
||||
username: loginData.username,
|
||||
password: loginData.password,
|
||||
clientId: loginData.clientId || ENCRYPT_CONFIG.CLIENT_ID,
|
||||
grantType: loginData.grantType || 'password',
|
||||
};
|
||||
|
||||
console.log('📋 登录数据:', JSON.stringify(fullLoginData, null, 2));
|
||||
|
||||
// 3. AES加密登录数据
|
||||
const encryptedData = encryptWithAES(JSON.stringify(fullLoginData), aesKey);
|
||||
console.log('🔐 AES加密后数据长度:', encryptedData.length);
|
||||
|
||||
// 4. RSA加密AES密钥 - 异步操作
|
||||
const encryptedKey = await encryptAESKeyWithRSA(aesKey);
|
||||
if (!encryptedKey) {
|
||||
throw new Error('RSA加密AES密钥失败');
|
||||
}
|
||||
console.log('🔑 RSA加密后密钥长度:', encryptedKey.length);
|
||||
|
||||
// 5. 返回加密结果和请求头
|
||||
return {
|
||||
success: true,
|
||||
encryptedData: encryptedData,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
clientid: ENCRYPT_CONFIG.CLIENT_ID,
|
||||
'encrypt-key': encryptedKey,
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
isToken: 'false',
|
||||
isEncrypt: 'true',
|
||||
repeatSubmit: 'false',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('登录数据加密失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加密配置
|
||||
*/
|
||||
export function getEncryptConfig() {
|
||||
return ENCRYPT_CONFIG;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// @ts-nocheck
|
||||
import CryptoJS from 'crypto-js';
|
||||
import NodeRSA from 'node-rsa';
|
||||
|
||||
// 配置信息
|
||||
const ENCRYPT_CONFIG = {
|
||||
// RSA公钥 - 与后端保持一致
|
||||
RSA_PUBLIC_KEY: 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7h96MCAwEAAQ==',
|
||||
CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
|
||||
TENANT_ID: '000000'
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机AES密钥
|
||||
* @param {number} length 密钥长度,默认32位
|
||||
* @returns {string} AES密钥
|
||||
*/
|
||||
export function generateRandomAESKey(length = 32) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* AES加密数据
|
||||
* @param {string} data 待加密的数据
|
||||
* @param {string} key AES密钥
|
||||
* @returns {string} 加密后的数据
|
||||
*/
|
||||
export function encryptWithAES(data, key) {
|
||||
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
|
||||
const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return encrypted.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA加密AES密钥
|
||||
* @param {string} aesKey AES密钥
|
||||
* @returns {string|null} RSA加密后的AES密钥
|
||||
*/
|
||||
export function encryptAESKeyWithRSA(aesKey) {
|
||||
try {
|
||||
const pemKey = `-----BEGIN PUBLIC KEY-----\n${ENCRYPT_CONFIG.RSA_PUBLIC_KEY}\n-----END PUBLIC KEY-----`;
|
||||
const rsaKey = new NodeRSA(pemKey);
|
||||
rsaKey.setOptions({ encryptionScheme: 'pkcs1' });
|
||||
|
||||
// 将AES密钥转为Base64
|
||||
const aesKeyBase64 = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(aesKey));
|
||||
|
||||
// RSA加密
|
||||
return rsaKey.encrypt(aesKeyBase64, 'base64');
|
||||
} catch (error) {
|
||||
console.error('RSA加密失败:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录数据加密处理
|
||||
* @param {Object} loginData 登录数据对象
|
||||
* @returns {Object} 包含加密数据和请求头的对象
|
||||
*/
|
||||
export function encryptLoginData(loginData) {
|
||||
try {
|
||||
// 1. 生成AES密钥
|
||||
const aesKey = generateRandomAESKey(32);
|
||||
console.log('🔑 生成AES密钥:', aesKey);
|
||||
|
||||
// 2. 准备完整的登录数据
|
||||
const fullLoginData = {
|
||||
tenantId: loginData.tenantId || ENCRYPT_CONFIG.TENANT_ID,
|
||||
username: loginData.username,
|
||||
password: loginData.password,
|
||||
clientId: loginData.clientId || ENCRYPT_CONFIG.CLIENT_ID,
|
||||
grantType: loginData.grantType || 'password',
|
||||
};
|
||||
|
||||
console.log('📋 登录数据:', JSON.stringify(fullLoginData, null, 2));
|
||||
|
||||
// 3. AES加密登录数据
|
||||
const encryptedData = encryptWithAES(JSON.stringify(fullLoginData), aesKey);
|
||||
console.log('🔐 AES加密后数据长度:', encryptedData.length);
|
||||
|
||||
// 4. RSA加密AES密钥
|
||||
const encryptedKey = encryptAESKeyWithRSA(aesKey);
|
||||
if (!encryptedKey) {
|
||||
throw new Error('RSA加密AES密钥失败');
|
||||
}
|
||||
console.log('🔑 RSA加密后密钥长度:', encryptedKey.length);
|
||||
|
||||
// 5. 返回加密结果和请求头
|
||||
return {
|
||||
success: true,
|
||||
encryptedData: encryptedData,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
clientid: ENCRYPT_CONFIG.CLIENT_ID,
|
||||
'encrypt-key': encryptedKey,
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
isToken: 'false',
|
||||
isEncrypt: 'true',
|
||||
repeatSubmit: 'false',
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('登录数据加密失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加密配置
|
||||
*/
|
||||
export function getEncryptConfig() {
|
||||
return ENCRYPT_CONFIG;
|
||||
}
|
||||
@@ -30,8 +30,13 @@ export const request = (options) => {
|
||||
if (res.data && res.data.code !== undefined) {
|
||||
if (res.data.code === 200) {
|
||||
resolve(res.data);
|
||||
} else if (res.data.code === 401) {
|
||||
// 401未授权,跳转到登录页
|
||||
console.log('🔒 Token失效,自动跳转到登录页');
|
||||
handleUnauthorized(res.data.msg || '登录已失效,请重新登录');
|
||||
reject(res.data);
|
||||
} else {
|
||||
// 业务错误
|
||||
// 其他业务错误
|
||||
uni.showToast({
|
||||
title: res.data.msg || '请求失败',
|
||||
icon: 'none',
|
||||
@@ -41,8 +46,13 @@ export const request = (options) => {
|
||||
} else {
|
||||
resolve(res.data);
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
// HTTP 401未授权
|
||||
console.log('🔒 HTTP 401未授权,自动跳转到登录页');
|
||||
handleUnauthorized('登录已失效,请重新登录');
|
||||
reject(res);
|
||||
} else {
|
||||
// HTTP状态码错误
|
||||
// 其他HTTP状态码错误
|
||||
uni.showToast({
|
||||
title: `请求失败: ${res.statusCode}`,
|
||||
icon: 'none',
|
||||
@@ -76,6 +86,51 @@ function getToken() {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理401未授权响应
|
||||
function handleUnauthorized(message = '登录已失效,请重新登录') {
|
||||
console.log('🔒 处理401未授权响应:', message);
|
||||
|
||||
// 清除登录信息
|
||||
try {
|
||||
uni.removeStorageSync('access_token');
|
||||
uni.removeStorageSync('userInfo');
|
||||
uni.removeStorageSync('isLoggedIn');
|
||||
console.log('📤 已清除本地登录信息');
|
||||
} catch (e) {
|
||||
console.error('❌ 清除登录信息失败:', e);
|
||||
}
|
||||
|
||||
// 显示提示信息
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
// 延迟跳转,确保Toast显示
|
||||
setTimeout(() => {
|
||||
// 获取当前页面路径
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const currentPath = '/' + currentPage.route;
|
||||
|
||||
// 如果当前不是登录页,则跳转到登录页
|
||||
if (currentPath !== '/pages/login/index') {
|
||||
console.log('🔄 从', currentPath, '跳转到登录页');
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index',
|
||||
fail: (error) => {
|
||||
console.error('❌ 跳转登录页失败:', error);
|
||||
// 如果reLaunch失败,尝试其他方式
|
||||
uni.redirectTo({
|
||||
url: '/pages/login/index',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// API方法封装
|
||||
export const api = {
|
||||
// 用户登录
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* 全局路由守卫
|
||||
* 处理页面访问权限控制和登录状态检查
|
||||
*/
|
||||
|
||||
import { userStorage } from './request';
|
||||
import { userState } from './user';
|
||||
|
||||
// 无需登录验证的页面列表
|
||||
const PUBLIC_PAGES = [
|
||||
'/pages/login/index',
|
||||
'/pages/index/index', // 首页作为公开页面,可以访问但功能受限
|
||||
];
|
||||
|
||||
// 登录页路径
|
||||
const LOGIN_PAGE = '/pages/login/index';
|
||||
|
||||
/**
|
||||
* 检查页面是否为公开页面
|
||||
* @param {string} path 页面路径
|
||||
*/
|
||||
function isPublicPage(path) {
|
||||
return PUBLIC_PAGES.some((publicPath) => {
|
||||
// 精确匹配或前缀匹配
|
||||
return path === publicPath || path.startsWith(publicPath + '?');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前页面路径
|
||||
*/
|
||||
function getCurrentPagePath() {
|
||||
try {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1];
|
||||
return '/' + currentPage.route;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取当前页面路径失败:', e);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行登录检查和跳转
|
||||
* @param {string} redirectReason 跳转原因
|
||||
*/
|
||||
function redirectToLogin(redirectReason = '请先登录') {
|
||||
console.log('🔄 重定向到登录页:', redirectReason);
|
||||
|
||||
// 清除可能存在的无效登录信息
|
||||
userStorage.clearLoginInfo();
|
||||
|
||||
// 提示用户
|
||||
uni.showToast({
|
||||
title: redirectReason,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
// 延迟跳转,确保Toast显示
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: LOGIN_PAGE,
|
||||
fail: (error) => {
|
||||
console.error('跳转登录页失败:', error);
|
||||
// 如果reLaunch失败,尝试其他方式
|
||||
uni.redirectTo({
|
||||
url: LOGIN_PAGE,
|
||||
});
|
||||
},
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局页面守卫检查
|
||||
* 在每个页面的onLoad或onShow中调用
|
||||
* @param {string} pagePath 页面路径(可选,自动获取当前页面)
|
||||
*/
|
||||
export function checkPageAccess(pagePath = null) {
|
||||
try {
|
||||
// 获取页面路径
|
||||
const currentPath = pagePath || getCurrentPagePath();
|
||||
console.log('🔍 检查页面访问权限:', currentPath);
|
||||
|
||||
// 如果是公开页面,允许访问
|
||||
if (isPublicPage(currentPath)) {
|
||||
console.log('✅ 公开页面,允许访问');
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
if (!userStorage.isLoggedIn()) {
|
||||
console.log('❌ 用户未登录');
|
||||
redirectToLogin('请先登录');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查Token是否存在
|
||||
const token = userStorage.getToken();
|
||||
if (!token) {
|
||||
console.log('❌ Token不存在');
|
||||
redirectToLogin('登录信息无效,请重新登录');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查Token是否过期
|
||||
if (userState.checkTokenExpiration()) {
|
||||
console.log('❌ Token已过期');
|
||||
redirectToLogin('登录已过期,请重新登录');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('✅ 页面访问权限验证通过');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('页面访问权限检查失败:', error);
|
||||
redirectToLogin('系统错误,请重新登录');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面导航守卫
|
||||
* 在页面跳转前进行权限检查
|
||||
* @param {string} url 目标页面URL
|
||||
* @param {object} options 跳转选项
|
||||
*/
|
||||
export function guardedNavigate(url, options = {}) {
|
||||
try {
|
||||
console.log('🚀 准备跳转到:', url);
|
||||
|
||||
// 解析URL获取页面路径
|
||||
const urlPath = url.split('?')[0];
|
||||
|
||||
// 如果目标是公开页面,直接跳转
|
||||
if (isPublicPage(urlPath)) {
|
||||
return uni.navigateTo({ url, ...options });
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
if (!userStorage.isLoggedIn()) {
|
||||
redirectToLogin('请先登录后访问');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查Token有效性
|
||||
const token = userStorage.getToken();
|
||||
if (!token || userState.checkTokenExpiration()) {
|
||||
redirectToLogin('登录已失效,请重新登录');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 执行跳转
|
||||
return uni.navigateTo({ url, ...options });
|
||||
} catch (error) {
|
||||
console.error('页面跳转守卫失败:', error);
|
||||
uni.showToast({
|
||||
title: '页面跳转失败',
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab页面切换守卫
|
||||
* @param {string} url Tab页面URL
|
||||
*/
|
||||
export function guardedSwitchTab(url) {
|
||||
try {
|
||||
console.log('🔄 准备切换Tab到:', url);
|
||||
|
||||
const urlPath = url.split('?')[0];
|
||||
|
||||
// 如果是公开Tab页面,允许切换
|
||||
if (isPublicPage(urlPath)) {
|
||||
return uni.switchTab({ url });
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
if (!userStorage.isLoggedIn() || !userStorage.getToken()) {
|
||||
redirectToLogin('请先登录');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查Token有效性
|
||||
if (userState.checkTokenExpiration()) {
|
||||
redirectToLogin('登录已过期,请重新登录');
|
||||
return false;
|
||||
}
|
||||
|
||||
return uni.switchTab({ url });
|
||||
} catch (error) {
|
||||
console.error('Tab切换守卫失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局混入方法,用于在页面中自动检查权限
|
||||
*/
|
||||
export const routeGuardMixin = {
|
||||
onLoad() {
|
||||
// 页面加载时检查权限
|
||||
this.checkPageAccess();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 页面显示时检查权限(防止后台切换回来时状态失效)
|
||||
this.checkPageAccess();
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 在页面中可直接调用的权限检查方法
|
||||
checkPageAccess() {
|
||||
return checkPageAccess();
|
||||
},
|
||||
|
||||
// 带权限检查的页面跳转
|
||||
guardedNavigate(url, options) {
|
||||
return guardedNavigate(url, options);
|
||||
},
|
||||
|
||||
// 带权限检查的Tab切换
|
||||
guardedSwitchTab(url) {
|
||||
return guardedSwitchTab(url);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化路由守卫
|
||||
* 在App.vue中调用
|
||||
*/
|
||||
export function initRouteGuard() {
|
||||
console.log('🛡️ 初始化全局路由守卫');
|
||||
|
||||
// 页面加载完成后检查当前页面权限
|
||||
uni.$on('pageLoad', () => {
|
||||
checkPageAccess();
|
||||
});
|
||||
|
||||
// 监听应用进入前台
|
||||
uni.$on('onShow', () => {
|
||||
console.log('📱 应用进入前台,检查登录状态');
|
||||
checkPageAccess();
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
checkPageAccess,
|
||||
guardedNavigate,
|
||||
guardedSwitchTab,
|
||||
routeGuardMixin,
|
||||
initRouteGuard,
|
||||
};
|
||||
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* 用户管理工具
|
||||
* 处理用户信息、权限、状态等相关功能
|
||||
*/
|
||||
|
||||
import { userStorage } from './request';
|
||||
|
||||
/**
|
||||
* 用户信息管理
|
||||
*/
|
||||
export const userManager = {
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
getCurrentUser() {
|
||||
const userInfo = userStorage.getUserInfo();
|
||||
if (!userInfo || !userInfo.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = userInfo.user;
|
||||
const permissions = userInfo.permissions || [];
|
||||
const roles = userInfo.roles || [];
|
||||
|
||||
return {
|
||||
// 基本信息
|
||||
userId: user.userId,
|
||||
userName: user.userName,
|
||||
nickName: user.nickName || user.userName,
|
||||
email: user.email,
|
||||
phoneNumber: user.phoneNumber,
|
||||
sex: user.sex,
|
||||
avatar: user.avatar,
|
||||
|
||||
// 部门信息
|
||||
deptId: user.deptId,
|
||||
deptName: user.dept?.deptName || '',
|
||||
|
||||
// 状态信息
|
||||
status: user.status,
|
||||
createTime: user.createTime,
|
||||
loginDate: user.loginDate,
|
||||
|
||||
// 权限信息
|
||||
permissions,
|
||||
roles,
|
||||
|
||||
// 扩展信息
|
||||
remark: user.remark,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查用户是否拥有指定权限
|
||||
* @param {string} permission 权限标识
|
||||
*/
|
||||
hasPermission(permission) {
|
||||
if (!permission) return true;
|
||||
|
||||
const userInfo = this.getCurrentUser();
|
||||
if (!userInfo || !userInfo.permissions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return userInfo.permissions.includes(permission);
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查用户是否拥有指定角色
|
||||
* @param {string} role 角色标识
|
||||
*/
|
||||
hasRole(role) {
|
||||
if (!role) return true;
|
||||
|
||||
const userInfo = this.getCurrentUser();
|
||||
if (!userInfo || !userInfo.roles) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return userInfo.roles.some((r) => r.roleKey === role);
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查用户是否为管理员
|
||||
*/
|
||||
isAdmin() {
|
||||
return this.hasRole('admin') || this.hasRole('super_admin');
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户显示名称
|
||||
*/
|
||||
getDisplayName() {
|
||||
const user = this.getCurrentUser();
|
||||
if (!user) return '未登录';
|
||||
|
||||
return user.nickName || user.userName || '用户';
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户头像URL
|
||||
* @param {string} defaultAvatar 默认头像URL
|
||||
*/
|
||||
getAvatarUrl(defaultAvatar = '/static/default-avatar.png') {
|
||||
const user = this.getCurrentUser();
|
||||
if (!user || !user.avatar) {
|
||||
return defaultAvatar;
|
||||
}
|
||||
|
||||
// 如果是完整URL,直接返回
|
||||
if (user.avatar.startsWith('http')) {
|
||||
return user.avatar;
|
||||
}
|
||||
|
||||
// 拼接基础URL
|
||||
const baseURL =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? 'http://localhost:8080'
|
||||
: 'https://your-api-domain.com';
|
||||
|
||||
return baseURL + user.avatar;
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新用户本地信息
|
||||
* @param {object} userData 用户数据
|
||||
*/
|
||||
updateUserInfo(userData) {
|
||||
try {
|
||||
const currentInfo = userStorage.getUserInfo();
|
||||
if (currentInfo) {
|
||||
const updatedInfo = {
|
||||
...currentInfo,
|
||||
user: {
|
||||
...currentInfo.user,
|
||||
...userData,
|
||||
},
|
||||
};
|
||||
uni.setStorageSync('userInfo', JSON.stringify(updatedInfo));
|
||||
console.log('用户信息已更新');
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('更新用户信息失败:', e);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化用户信息用于显示
|
||||
*/
|
||||
formatUserForDisplay() {
|
||||
const user = this.getCurrentUser();
|
||||
if (!user) return null;
|
||||
|
||||
return {
|
||||
name: this.getDisplayName(),
|
||||
avatar: this.getAvatarUrl(),
|
||||
department: user.deptName || '未分配部门',
|
||||
phone: user.phoneNumber || '未设置',
|
||||
email: user.email || '未设置',
|
||||
status: user.status === '0' ? '正常' : '停用',
|
||||
isAdmin: this.isAdmin(),
|
||||
lastLoginTime: user.loginDate ? new Date(user.loginDate).toLocaleString() : '首次登录',
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 权限检查指令工具
|
||||
*/
|
||||
export const permissionUtils = {
|
||||
/**
|
||||
* 检查页面访问权限
|
||||
* @param {string} pagePath 页面路径
|
||||
*/
|
||||
checkPagePermission(pagePath) {
|
||||
// 公开页面,无需权限检查
|
||||
const publicPages = ['/pages/login/index', '/pages/index/index', '/pages/profile/index'];
|
||||
|
||||
if (publicPages.includes(pagePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否已登录
|
||||
if (!userStorage.isLoggedIn()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 这里可以根据具体业务需求添加页面权限检查逻辑
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* 权限控制的页面跳转
|
||||
* @param {string} url 目标页面URL
|
||||
* @param {string} permission 所需权限
|
||||
*/
|
||||
navigateWithPermission(url, permission = null) {
|
||||
// 检查登录状态
|
||||
if (!userStorage.isLoggedIn()) {
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none',
|
||||
});
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (permission && !userManager.hasPermission(permission)) {
|
||||
uni.showToast({
|
||||
title: '权限不足',
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// 执行跳转
|
||||
uni.navigateTo({
|
||||
url,
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '页面跳转失败',
|
||||
icon: 'none',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 用户状态管理
|
||||
*/
|
||||
export const userState = {
|
||||
/**
|
||||
* 检查Token是否即将过期(提前5分钟提醒)
|
||||
*/
|
||||
checkTokenExpiration() {
|
||||
try {
|
||||
const token = userStorage.getToken();
|
||||
if (!token) return false;
|
||||
|
||||
// 这里可以解析JWT token获取过期时间
|
||||
// 简化处理:检查登录时间是否超过2小时
|
||||
const userInfo = userStorage.getUserInfo();
|
||||
if (userInfo && userInfo.loginTime) {
|
||||
const loginTime = new Date(userInfo.loginTime).getTime();
|
||||
const now = Date.now();
|
||||
const twoHours = 2 * 60 * 60 * 1000; // 2小时
|
||||
|
||||
return now - loginTime > twoHours;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('检查Token过期时间失败:', e);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*/
|
||||
async refreshToken() {
|
||||
try {
|
||||
// 这里调用刷新Token的API
|
||||
// const result = await api.refreshToken();
|
||||
// if (result.code === 200) {
|
||||
// uni.setStorageSync('access_token', result.data.access_token);
|
||||
// return true;
|
||||
// }
|
||||
console.log('Token刷新功能待实现');
|
||||
} catch (e) {
|
||||
console.error('刷新Token失败:', e);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 自动登录检查
|
||||
*/
|
||||
autoLoginCheck() {
|
||||
if (!userStorage.isLoggedIn()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查Token是否过期
|
||||
if (this.checkTokenExpiration()) {
|
||||
uni.showModal({
|
||||
title: '登录过期',
|
||||
content: '您的登录已过期,请重新登录',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
userStorage.clearLoginInfo();
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index',
|
||||
});
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
userManager,
|
||||
permissionUtils,
|
||||
userState,
|
||||
};
|
||||
Reference in new issue
Block a user