删除不需要的文件及提交移动端的代码

This commit is contained in:
zlwu committed 2025-08-06 16:01:27 +08:00
1 parent 86f65df783
commit 134f711349
751 files changed
+3888 -115501

No files matched your search

+36 -2
View File
@@ -7,8 +7,9 @@
tabindex="0"
>
<view class="service-icon">
<text class="icon" v-if="icon">{{ icon }}</text>
<slot v-else name="icon"></slot>
<slot name="icon">
<text class="icon" v-if="icon">{{ icon }}</text>
</slot>
</view>
<text class="service-title">{{ title }}</text>
<view v-if="badge" class="service-badge">
@@ -82,12 +83,33 @@ export default {
.icon {
font-size: 24px;
}
// 支持u-icon组件
:deep(.u-icon) {
font-size: 24px !important;
color: inherit !important;
}
// 支持iconfont图标
.iconfont {
width: 24px;
height: 24px;
font-size: 24px;
color: inherit;
filter: brightness(0) saturate(100%) invert(49%) sepia(98%) saturate(3332%)
hue-rotate(196deg) brightness(104%) contrast(101%);
}
}
.service-item:hover .service-icon {
background: $uni-color-primary;
color: $uni-text-color-inverse;
transform: scale(1.1);
.iconfont {
filter: brightness(0) saturate(100%) invert(100%) sepia(0%) saturate(0%) hue-rotate(0deg)
brightness(100%) contrast(100%);
}
}
.service-title {
@@ -160,6 +182,18 @@ export default {
.icon {
font-size: 20px;
}
// 响应式u-icon支持
:deep(.u-icon) {
font-size: 20px !important;
}
// 响应式iconfont支持
.iconfont {
width: 20px;
height: 20px;
font-size: 20px;
}
}
.service-title {
+8 -44
View File
@@ -24,20 +24,6 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/workbench/index",
"style": {
"navigationBarTitleText": "办公中心",
"navigationStyle": "custom"
}
},
{
"path": "pages/profile/index",
"style": {
"navigationBarTitleText": "我的",
"navigationStyle": "custom"
}
},
{
"path": "pages/iconfont-demo/index",
"style": {
@@ -66,6 +52,14 @@
"navigationBarTitleText": "我发起的流程",
"navigationStyle": "custom"
}
},
{
"path": "pages/uview-icon-test/index",
"style": {
"navigationBarTitleText": "uview图标测试",
"navigationBarBackgroundColor": "#5d87ff",
"navigationBarTextStyle": "white"
}
}
],
"globalStyle": {
@@ -73,35 +67,5 @@
"navigationBarTitleText": "智慧校园",
"navigationBarBackgroundColor": "rgb(93, 135, 255)",
"backgroundColor": "#f8f9fa"
},
"tabBar": {
"color": "#999999",
"selectedColor": "rgb(93, 135, 255)",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"height": "60px",
"fontSize": "12px",
"iconWidth": "22px",
"iconHeight": "22px",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/tabbar/home.svg",
"selectedIconPath": "static/tabbar/home-active.svg"
},
{
"pagePath": "pages/workbench/index",
"text": "办公中心",
"iconPath": "static/tabbar/workbench.svg",
"selectedIconPath": "static/tabbar/workbench-active.svg"
},
{
"pagePath": "pages/profile/index",
"text": "我的",
"iconPath": "static/tabbar/profile.svg",
"selectedIconPath": "static/tabbar/profile-active.svg"
}
]
}
}
@@ -38,9 +38,19 @@
<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>
<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-edit"></text>
<text class="icon-name">编辑</text>
<text class="icon-code">icon-edit</text>
</view>
<view class="icon-item">
<text class="iconfont icon-delete"></text>
<text class="icon-name">删除</text>
<text class="icon-code">icon-delete</text>
</view>
<view class="icon-item">
<text class="iconfont icon-search"></text>
@@ -48,9 +58,9 @@
<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>
<text class="iconfont icon-refresh"></text>
<text class="icon-name">刷新</text>
<text class="icon-code">icon-refresh</text>
</view>
<view class="icon-item">
<text class="iconfont icon-close"></text>
@@ -60,6 +70,82 @@
</view>
</view>
<!-- 文件图标 -->
<view class="icon-section">
<text class="section-title">文件图标</text>
<view class="icon-grid">
<view class="icon-item">
<text class="iconfont icon-download"></text>
<text class="icon-name">下载</text>
<text class="icon-code">icon-download</text>
</view>
<view class="icon-item">
<text class="iconfont icon-upload"></text>
<text class="icon-name">上传</text>
<text class="icon-code">icon-upload</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-success" style="color: #52c41a"></text>
<text class="icon-name">成功</text>
<text class="icon-code">icon-success</text>
</view>
<view class="icon-item">
<text class="iconfont icon-warning" style="color: #faad14"></text>
<text class="icon-name">警告</text>
<text class="icon-code">icon-warning</text>
</view>
<view class="icon-item">
<text class="iconfont icon-info" style="color: #1890ff"></text>
<text class="icon-name">信息</text>
<text class="icon-code">icon-info</text>
</view>
<view class="icon-item">
<text class="iconfont icon-check" style="color: #52c41a"></text>
<text class="icon-name">确认</text>
<text class="icon-code">icon-check</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-star" style="color: #faad14"></text>
<text class="icon-name">星星</text>
<text class="icon-code">icon-star</text>
</view>
<view class="icon-item">
<text class="iconfont icon-heart" style="color: #ff4d4f"></text>
<text class="icon-name">喜欢</text>
<text class="icon-code">icon-heart</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-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>
</view>
<!-- 导航图标 -->
<view class="icon-section">
<text class="section-title">导航图标</text>
@@ -0,0 +1,209 @@
<template>
<view class="test-page">
<view class="test-header">
<text class="test-title">uview-plus 图标问题分析与解决</text>
</view>
<!-- 问题演示 -->
<view class="test-section">
<text class="section-title">❌ 问题演示 - uview-plus图标异常</text>
<view class="test-items">
<view class="test-item">
<u-icon name="logout" size="24" color="#ff4d4f"></u-icon>
<text class="test-desc">logout图标 (可能显示文字)</text>
</view>
<view class="test-item">
<u-icon name="power" size="24" color="#ff4d4f"></u-icon>
<text class="test-desc">power图标 (可能显示文字)</text>
</view>
<view class="test-item">
<u-icon name="exit" size="24" color="#ff4d4f"></u-icon>
<text class="test-desc">exit图标 (可能显示文字)</text>
</view>
</view>
</view>
<!-- 正常工作的uview图标 -->
<view class="test-section">
<text class="section-title">✅ 正常工作 - uview-plus图标</text>
<view class="test-items">
<view class="test-item">
<u-icon name="account" size="24" color="#52c41a"></u-icon>
<text class="test-desc">account图标 (正常)</text>
</view>
<view class="test-item">
<u-icon name="home" size="24" color="#52c41a"></u-icon>
<text class="test-desc">home图标 (正常)</text>
</view>
<view class="test-item">
<u-icon name="setting" size="24" color="#52c41a"></u-icon>
<text class="test-desc">setting图标 (正常)</text>
</view>
</view>
</view>
<!-- 解决方案 - 使用iconfont -->
<view class="test-section">
<text class="section-title">🎯 解决方案 - 使用iconfont自定义图标</text>
<view class="test-items">
<view class="test-item">
<text class="iconfont icon-close solution-icon"></text>
<text class="test-desc">关闭图标 (退出登录)</text>
</view>
<view class="test-item">
<text class="iconfont icon-user solution-icon"></text>
<text class="test-desc">用户图标</text>
</view>
<view class="test-item">
<text class="iconfont icon-setting solution-icon"></text>
<text class="test-desc">设置图标</text>
</view>
</view>
</view>
<!-- Unicode字符方案 -->
<view class="test-section">
<text class="section-title">🔤 备选方案 - Unicode字符</text>
<view class="test-items">
<view class="test-item">
<text class="unicode-icon">⏻</text>
<text class="test-desc">电源符号 (退出登录)</text>
</view>
<view class="test-item">
<text class="unicode-icon">❌</text>
<text class="test-desc">关闭符号</text>
</view>
<view class="test-item">
<text class="unicode-icon">⚙️</text>
<text class="test-desc">设置符号</text>
</view>
</view>
</view>
<!-- 解决方案说明 -->
<view class="solution-section">
<text class="section-title">💡 解决方案说明</text>
<view class="solution-content">
<text class="solution-text">
1. **根本原因**: uview-plus某些图标名称在当前版本字体文件中不存在
</text>
<text class="solution-text">
2. **推荐方案**: 使用项目中的iconfont自定义图标系统
</text>
<text class="solution-text">
3. **备选方案**: 使用Unicode字符(⏻ ❌ ⚙️),兼容性最好
</text>
<text class="solution-text">
4. **长期方案**: 更新uview-plus字体文件或使用其他图标库
</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'UviewIconTest',
data() {
return {};
},
};
</script>
<style lang="scss" scoped>
.test-page {
min-height: 100vh;
background: #f8f9fa;
padding: 20px;
}
.test-header {
text-align: center;
margin-bottom: 32px;
}
.test-title {
display: block;
font-size: 20px;
font-weight: bold;
color: #333;
}
.test-section {
background: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.section-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
}
.test-items {
display: flex;
flex-direction: column;
gap: 12px;
}
.test-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #fafbfc;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.test-desc {
font-size: 14px;
color: #666;
}
.solution-icon {
font-size: 24px;
color: #1890ff;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.unicode-icon {
font-size: 24px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.solution-section {
background: #fff7e6;
border: 1px solid #ffd591;
border-radius: 12px;
padding: 20px;
}
.solution-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.solution-text {
display: block;
font-size: 14px;
color: #333;
line-height: 1.6;
}
</style>
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -19,7 +19,7 @@
v-model="username"
placeholder="账号"
type="text"
:border="true"
border="true"
:custom-style="inputStyle"
placeholder-style="color: #999; font-size: 16px"
>
@@ -34,7 +34,7 @@
v-model="password"
placeholder="密码"
type="password"
:border="true"
border="true"
:custom-style="inputStyle"
placeholder-style="color: #999; font-size: 16px"
@confirm="onPasswordConfirm"
@@ -187,7 +187,7 @@ export default {
// 调用登录API
const loginResult = await api.login(loginData);
console.log('登录结果:', loginResult);
if (loginResult.code === 200) {
// 登录成功,先保存Token
console.log('✅ 登录成功,先保存Token');
-674
View File
@@ -1,674 +0,0 @@
<template>
<view class="page-container">
<!-- 自定义导航栏 -->
<NavBar title="我的" :show-settings="true" @settings="handleSettings" />
<!-- 主要内容区域 -->
<scroll-view class="main-content" scroll-y enhanced :show-scrollbar="false">
<!-- 用户信息卡片 -->
<view class="profile-card">
<view class="profile-info">
<view class="profile-avatar">
<text class="avatar-icon">👤</text>
</view>
<view class="profile-details">
<text class="user-name">{{ userInfo.name }}</text>
<text class="user-major">{{ userInfo.major }} · {{ userInfo.grade }}</text>
<text class="user-id">学号:{{ userInfo.studentId }}</text>
</view>
</view>
<view class="profile-stats">
<view class="stat-item">
<text class="stat-number">{{ userInfo.gpa }}</text>
<text class="stat-label">GPA</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ userInfo.credits }}</text>
<text class="stat-label">学分</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ userInfo.grade }}</text>
<text class="stat-label">年级</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ userInfo.ranking }}</text>
<text class="stat-label">排名</text>
</view>
</view>
</view>
<!-- 学习状态 -->
<Card>
<template #header>
<view class="section-header">
<text class="section-icon">📊</text>
<text class="card-title">学习状态</text>
</view>
</template>
<GridLayout :columns="2" gap="12px">
<StatCard
v-for="stat in learningStats"
:key="stat.label"
:number="stat.number"
:label="stat.label"
@click="handleStatClick"
/>
</GridLayout>
</Card>
<!-- 我的服务 -->
<view class="menu-section">
<view
v-for="menu in myServiceMenus"
:key="menu.title"
class="menu-item touchable"
@click="handleMenuClick(menu)"
>
<view class="menu-icon">
<text class="icon">{{ menu.icon }}</text>
</view>
<view class="menu-content">
<text class="menu-title">{{ menu.title }}</text>
<text class="menu-desc">{{ menu.description }}</text>
</view>
<text class="menu-arrow">›</text>
</view>
</view>
<view class="section-divider"></view>
<!-- 校园生活 -->
<view class="menu-section">
<view
v-for="menu in campusLifeMenus"
:key="menu.title"
class="menu-item touchable"
@click="handleMenuClick(menu)"
>
<view class="menu-icon">
<text class="icon">{{ menu.icon }}</text>
</view>
<view class="menu-content">
<text class="menu-title">{{ menu.title }}</text>
<text class="menu-desc">{{ menu.description }}</text>
</view>
<text class="menu-arrow">›</text>
</view>
</view>
<view class="section-divider"></view>
<!-- 应用设置 -->
<view class="menu-section">
<view
v-for="menu in settingsMenus"
:key="menu.title"
class="menu-item touchable"
@click="handleMenuClick(menu)"
>
<view class="menu-icon">
<text class="icon">{{ menu.icon }}</text>
</view>
<view class="menu-content">
<text class="menu-title">{{ menu.title }}</text>
<text class="menu-desc">{{ menu.description }}</text>
</view>
<text class="menu-arrow">›</text>
</view>
</view>
<view class="section-divider"></view>
<!-- 帮助与反馈 -->
<view class="menu-section">
<view
v-for="menu in helpMenus"
:key="menu.title"
class="menu-item touchable"
@click="handleMenuClick(menu)"
>
<view class="menu-icon">
<text class="icon">{{ menu.icon }}</text>
</view>
<view class="menu-content">
<text class="menu-title">{{ menu.title }}</text>
<text class="menu-desc">{{ menu.description }}</text>
</view>
<text class="menu-arrow">›</text>
</view>
</view>
<!-- 版权信息 -->
<view class="footer-section">
<text class="app-version">智慧校园 v2.1.0</text>
<text class="copyright">© 2024 University. All rights reserved.</text>
</view>
<!-- 底部间距 -->
<view class="bottom-spacer"></view>
</scroll-view>
</view>
</template>
<script>
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',
components: {
NavBar,
Card,
StatCard,
GridLayout,
},
data() {
return {
userInfo: {
name: '用户',
major: '未设置',
grade: '未设置',
studentId: '未设置',
gpa: 0,
credits: 0,
ranking: 0,
},
learningStats: [
{ number: 5, label: '本周课程' },
{ number: 3, label: '待提交作业' },
{ number: 2, label: '即将考试' },
{ number: 8, label: '借阅图书' },
],
myServiceMenus: [
{
title: '我的课表',
description: '查看本学期课程安排',
icon: '🎓',
action: 'schedule',
},
{
title: '成绩单',
description: '查看历史成绩记录',
icon: '📊',
action: 'grades',
},
{
title: '我的待办',
description: '管理学习任务和事项',
icon: '✅',
action: 'todos',
},
{
title: '校园卡',
description: '余额:¥128.50',
icon: '💳',
action: 'card',
},
],
campusLifeMenus: [
{
title: '图书借阅',
description: '当前借阅8本,2本即将到期',
icon: '📚',
action: 'library',
},
{
title: '宿舍信息',
description: '1号楼 308室',
icon: '🛏️',
action: 'dormitory',
},
{
title: '社团活动',
description: '计算机协会、摄影社',
icon: '👥',
action: 'clubs',
},
{
title: '活动报名',
description: '查看可参与的活动',
icon: '📅',
action: 'events',
},
],
settingsMenus: [
{
title: '个人信息',
description: '编辑个人资料',
icon: '👤',
action: 'profile',
},
{
title: '消息通知',
description: '管理通知设置',
icon: '🔔',
action: 'notifications',
},
{
title: '账号安全',
description: '密码、绑定手机等',
icon: '🛡️',
action: 'security',
},
{
title: '主题设置',
description: '切换应用主题',
icon: '🎨',
action: 'theme',
},
],
helpMenus: [
{
title: '帮助中心',
description: '常见问题解答',
icon: '❓',
action: 'help',
},
{
title: '意见反馈',
description: '提交建议和问题',
icon: '💬',
action: 'feedback',
},
{
title: '关于应用',
description: '版本 v2.1.0',
icon: 'ℹ️',
action: 'about',
},
{
title: '退出登录',
description: '安全退出当前账号',
icon: '🚪',
action: 'logout',
},
],
};
},
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' });
},
handleStatClick(stat) {
uni.showToast({ title: `查看${stat.label}详情`, icon: 'none' });
},
handleMenuClick(menu) {
uni.vibrateShort();
switch (menu.action) {
case 'schedule':
this.openSchedule();
break;
case 'grades':
this.openGrades();
break;
case 'todos':
this.openTodos();
break;
case 'card':
this.openCampusCard();
break;
case 'library':
this.openLibrary();
break;
case 'logout':
this.handleLogout();
break;
default:
uni.showToast({
title: `${menu.title}功能开发中`,
icon: 'none',
});
}
},
openSchedule() {
uni.showToast({ title: '打开课程表', icon: 'success' });
},
openGrades() {
uni.showToast({ title: '打开成绩单', icon: 'success' });
},
openTodos() {
uni.showToast({ title: '打开待办事项', icon: 'success' });
},
openCampusCard() {
uni.showToast({ title: '打开校园卡', icon: 'success' });
},
openLibrary() {
uni.showToast({ title: '打开图书借阅', icon: 'success' });
},
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);
}
},
});
},
},
onLoad() {
// 检查页面访问权限
if (!checkPageAccess()) {
return;
}
if (this.checkLoginStatus()) {
this.loadUserInfo();
}
},
onShow() {
// 页面显示时再次检查权限
if (!checkPageAccess()) {
return;
}
// 刷新用户信息
this.loadUserInfo();
},
};
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: $uni-bg-color-grey;
display: flex;
flex-direction: column;
}
.main-content {
flex: 1;
padding-bottom: calc(50px + env(safe-area-inset-bottom));
}
/* 用户信息卡片 */
.profile-card {
background: linear-gradient(135deg, $uni-color-primary, $uni-color-primary-dark);
color: $uni-text-color-inverse;
margin: $uni-spacing-col-lg;
border-radius: $uni-border-radius-lg;
padding: 24px 20px;
box-shadow: $uni-shadow;
}
.profile-info {
display: flex;
align-items: center;
margin-bottom: $uni-spacing-col-lg;
}
.profile-avatar {
width: 60px;
height: 60px;
border-radius: $uni-border-radius-circle;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
margin-right: $uni-spacing-row-lg;
}
.avatar-icon {
font-size: 24px;
color: $uni-text-color-inverse;
}
.profile-details {
flex: 1;
display: flex;
flex-direction: column;
}
.user-name {
font-size: $uni-font-size-xl;
font-weight: 600;
margin-bottom: 4px;
}
.user-major {
font-size: $uni-font-size-base;
opacity: 0.9;
margin-bottom: 4px;
}
.user-id {
font-size: $uni-font-size-sm;
opacity: 0.8;
}
.profile-stats {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: $uni-spacing-col-lg;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
.stat-item {
text-align: center;
flex: 1;
}
.stat-number {
font-size: $uni-font-size-xxl;
font-weight: 600;
display: block;
margin-bottom: 4px;
}
.stat-label {
font-size: $uni-font-size-sm;
opacity: 0.8;
}
/* 分组标题 */
.section-header {
display: flex;
align-items: center;
gap: 8px;
}
.section-icon {
font-size: $uni-font-size-lg;
color: $uni-color-primary;
}
/* 菜单列表 */
.menu-section {
background: $uni-bg-color;
border-radius: $uni-border-radius-lg;
margin: $uni-spacing-col-lg;
box-shadow: $uni-shadow;
overflow: hidden;
}
.menu-item {
display: flex;
align-items: center;
padding: $uni-spacing-col-lg $uni-spacing-row-xl;
transition: $uni-transition;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
&:hover {
background: $uni-color-primary-light;
}
}
.menu-icon {
width: 40px;
height: 40px;
background: $uni-color-primary-light;
border-radius: $uni-border-radius-circle;
display: flex;
align-items: center;
justify-content: center;
margin-right: $uni-spacing-row-lg;
.icon {
font-size: 18px;
color: $uni-color-primary;
}
}
.menu-content {
flex: 1;
display: flex;
flex-direction: column;
}
.menu-title {
font-size: $uni-font-size-lg;
font-weight: 500;
color: $uni-text-color;
margin-bottom: 2px;
}
.menu-desc {
font-size: $uni-font-size-sm;
color: $uni-text-color-secondary;
}
.menu-arrow {
color: $uni-text-color-grey;
font-size: $uni-font-size-xl;
font-weight: 300;
}
/* 分隔器 */
.section-divider {
height: 8px;
background: $uni-bg-color-grey;
margin: 0 $uni-spacing-row-lg;
}
/* 版权信息 */
.footer-section {
padding: 24px;
text-align: center;
color: $uni-text-color-grey;
}
.app-version {
font-size: $uni-font-size-sm;
display: block;
margin-bottom: 8px;
}
.copyright {
font-size: 11px;
}
/* 底部间距 */
.bottom-spacer {
height: 20px;
}
/* 菜单项进入动画 */
.menu-item {
animation: slideInRight 0.6s ease-out backwards;
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* 页面进入动画 */
.page-container {
animation: fadeInUp 0.6s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */
@media (max-width: 375px) {
.profile-card,
.menu-section {
margin: $uni-spacing-col-base;
}
.menu-item {
padding: $uni-spacing-col-base $uni-spacing-row-lg;
}
.profile-avatar {
width: 50px;
height: 50px;
.avatar-icon {
font-size: 20px;
}
}
.user-name {
font-size: $uni-font-size-lg;
}
.stat-number {
font-size: $uni-font-size-xl;
}
}
</style>
-499
View File
@@ -1,499 +0,0 @@
<template>
<view class="page-container">
<!-- 自定义导航栏 -->
<NavBar title="办公中心" :show-search="true" @search="handleSearch" />
<!-- 主要内容区域 -->
<scroll-view class="main-content" scroll-y enhanced :show-scrollbar="false">
<!-- 搜索栏 -->
<view class="search-section">
<view class="search-bar" @click="focusSearch">
<text class="search-icon">🔍</text>
<input
class="search-input"
placeholder="搜索服务..."
v-model="searchQuery"
@input="handleSearchInput"
@confirm="handleSearchConfirm"
:focus="searchFocused"
/>
</view>
</view>
<!-- 热门服务 -->
<Card>
<template #header>
<view class="section-header">
<text class="section-icon">🔥</text>
<text class="card-title">热门服务</text>
</view>
</template>
<GridLayout :columns="4" gap="12px">
<ServiceItem
v-for="service in hotServices"
:key="service.title"
:title="service.title"
:icon="service.icon"
:badge="service.badge"
@click="handleServiceClick"
/>
</GridLayout>
</Card>
<!-- 学习教务 -->
<Card>
<template #header>
<view class="section-header">
<text class="section-icon">🎓</text>
<text class="card-title">学习教务</text>
</view>
</template>
<GridLayout :columns="3" gap="12px">
<ServiceItem
v-for="service in academicServices"
:key="service.title"
:title="service.title"
:icon="service.icon"
@click="handleServiceClick"
/>
</GridLayout>
</Card>
<!-- 生活服务 -->
<Card>
<template #header>
<view class="section-header">
<text class="section-icon">🏠</text>
<text class="card-title">生活服务</text>
</view>
</template>
<GridLayout :columns="3" gap="12px">
<ServiceItem
v-for="service in lifeServices"
:key="service.title"
:title="service.title"
:icon="service.icon"
@click="handleServiceClick"
/>
</GridLayout>
</Card>
<!-- 校园资源 -->
<Card>
<template #header>
<view class="section-header">
<text class="section-icon">🏢</text>
<text class="card-title">校园资源</text>
</view>
</template>
<GridLayout :columns="3" gap="12px">
<ServiceItem
v-for="service in resourceServices"
:key="service.title"
:title="service.title"
:icon="service.icon"
@click="handleServiceClick"
/>
</GridLayout>
</Card>
<!-- 办事服务 -->
<Card>
<template #header>
<view class="section-header">
<text class="section-icon">📋</text>
<text class="card-title">办事服务</text>
</view>
</template>
<GridLayout :columns="3" gap="12px">
<ServiceItem
v-for="service in adminServices"
:key="service.title"
:title="service.title"
:icon="service.icon"
@click="handleServiceClick"
/>
</GridLayout>
</Card>
<!-- 信息查询 -->
<Card>
<template #header>
<view class="section-header">
<text class="section-icon">🔍</text>
<text class="card-title">信息查询</text>
</view>
</template>
<GridLayout :columns="3" gap="12px">
<ServiceItem
v-for="service in queryServices"
:key="service.title"
:title="service.title"
:icon="service.icon"
@click="handleServiceClick"
/>
</GridLayout>
</Card>
<!-- 快捷工具 -->
<Card>
<template #header>
<view class="section-header">
<text class="section-icon">⚡</text>
<text class="card-title">快捷工具</text>
</view>
</template>
<GridLayout :columns="3" gap="12px">
<ServiceItem
v-for="service in toolServices"
:key="service.title"
:title="service.title"
:icon="service.icon"
@click="handleServiceClick"
/>
</GridLayout>
</Card>
<!-- 底部间距 -->
<view class="bottom-spacer"></view>
</scroll-view>
</view>
</template>
<script>
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',
components: {
NavBar,
Card,
ServiceItem,
GridLayout,
},
data() {
return {
searchQuery: '',
searchFocused: false,
hotServices: [
{ title: '课程表', icon: '🎓' },
{ title: '成绩查询', icon: '📊' },
{ title: '校园卡', icon: '💳' },
{ title: '图书馆', icon: '📚' },
],
academicServices: [
{ title: '我的课表', icon: '🎓' },
{ title: '成绩查询', icon: '📊' },
{ title: '选课系统', icon: '👨‍🎓' },
{ title: '考试安排', icon: '📝' },
{ title: '课程评教', icon: '📋' },
{ title: '学分查询', icon: '🏆' },
{ title: '作业提交', icon: '📄' },
{ title: '竞赛报名', icon: '🏅' },
{ title: '教学评价', icon: '👨‍🏫' },
],
lifeServices: [
{ title: '食堂菜单', icon: '🍽️' },
{ title: '校园卡', icon: '💳' },
{ title: '宿舍管理', icon: '🛏️' },
{ title: '停车服务', icon: '🚗' },
{ title: '校车时刻', icon: '🚌' },
{ title: '校园购物', icon: '🛒' },
{ title: '医疗服务', icon: '🏥' },
{ title: '报修服务', icon: '🔧' },
{ title: '空调控制', icon: '🌡️' },
],
resourceServices: [
{ title: '图书馆', icon: '📚' },
{ title: '机房预约', icon: '💻' },
{ title: '会议室', icon: '🎤' },
{ title: '体育馆', icon: '🏃‍♂️' },
{ title: '艺术中心', icon: '🎭' },
{ title: '实验室', icon: '🧪' },
{ title: '打印服务', icon: '🖨️' },
{ title: '网络服务', icon: '📶' },
{ title: '设备租借', icon: '🔧' },
],
adminServices: [
{ title: '学费缴费', icon: '💰' },
{ title: '证明开具', icon: '📄' },
{ title: '学生证', icon: '🆔' },
{ title: '学籍管理', icon: '👤' },
{ title: '转专业', icon: '🔄' },
{ title: '请假申请', icon: '✈️' },
{ title: '毕业申请', icon: '🎓' },
{ title: '就业指导', icon: '💼' },
{ title: '国际交流', icon: '🌍' },
],
queryServices: [
{ title: '校历查询', icon: '📅' },
{ title: '校园通讯录', icon: '📞' },
{ title: '校园地图', icon: '🗺️' },
{ title: '通知公告', icon: '📢' },
{ title: '常见问题', icon: '❓' },
{ title: '意见反馈', icon: '💬' },
{ title: '在线客服', icon: '🎧' },
{ title: '数据统计', icon: '📈' },
{ title: '校园新闻', icon: '📰' },
],
toolServices: [
{ title: '扫一扫', icon: '📱' },
{ title: '计算器', icon: '🧮' },
{ title: '作息时间', icon: '⏰' },
{ title: '天气查询', icon: '☁️' },
{ title: '翻译工具', icon: '🔤' },
{ title: '分享', icon: '📤' },
],
};
},
methods: {
handleSearch() {
this.focusSearch();
},
focusSearch() {
this.searchFocused = true;
},
handleSearchInput(e) {
this.searchQuery = e.detail.value;
// 实现搜索建议逻辑
this.showSearchSuggestions();
},
handleSearchConfirm() {
if (this.searchQuery.trim()) {
this.performSearch(this.searchQuery);
}
},
showSearchSuggestions() {
// 根据输入显示搜索建议
if (this.searchQuery.length > 0) {
// 这里可以实现搜索建议逻辑
}
},
performSearch(query) {
uni.showToast({
title: `搜索: ${query}`,
icon: 'none',
duration: 2000,
});
this.searchFocused = false;
},
handleServiceClick(service) {
uni.vibrateShort();
// 根据不同服务类型进行路由跳转或功能调用
switch (service.title) {
case '课程表':
case '我的课表':
this.openCourseSchedule();
break;
case '成绩查询':
this.openGradeQuery();
break;
case '校园卡':
this.openCampusCard();
break;
case '图书馆':
this.openLibrary();
break;
case '扫一扫':
this.openScanner();
break;
default:
uni.showToast({
title: `${service.title}功能开发中`,
icon: 'none',
});
}
},
openCourseSchedule() {
uni.showToast({ title: '打开课程表', icon: 'success' });
// 这里可以跳转到课程表页面
},
openGradeQuery() {
uni.showToast({ title: '打开成绩查询', icon: 'success' });
// 这里可以跳转到成绩查询页面
},
openCampusCard() {
uni.showToast({ title: '打开校园卡', icon: 'success' });
// 这里可以跳转到校园卡页面
},
openLibrary() {
uni.showToast({ title: '打开图书馆', icon: 'success' });
// 这里可以跳转到图书馆页面
},
openScanner() {
uni.scanCode({
success: (res) => {
uni.showToast({
title: `扫描结果: ${res.result}`,
icon: 'none',
});
},
fail: () => {
uni.showToast({
title: '扫描失败',
icon: 'error',
});
},
});
},
// 检查登录状态
checkLoginStatus() {
if (!userStorage.isLoggedIn()) {
uni.reLaunch({
url: '/pages/login/index',
});
return;
}
},
},
onLoad() {
// 检查页面访问权限
if (!checkPageAccess()) {
return;
}
this.checkLoginStatus();
},
onShow() {
// 页面显示时再次检查权限
if (!checkPageAccess()) {
return;
}
},
};
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: $uni-bg-color-grey;
display: flex;
flex-direction: column;
}
.main-content {
flex: 1;
padding-bottom: calc(50px + env(safe-area-inset-bottom));
}
/* 搜索栏 */
.search-section {
padding: $uni-spacing-col-lg;
}
.search-bar {
position: relative;
background: $uni-bg-color;
border-radius: $uni-border-radius-lg;
box-shadow: $uni-shadow;
display: flex;
align-items: center;
padding: 0 $uni-spacing-row-lg;
transition: $uni-transition;
&:hover {
box-shadow: $uni-shadow-hover;
}
}
.search-icon {
font-size: 16px;
color: $uni-text-color-grey;
margin-right: $uni-spacing-row-base;
}
.search-input {
flex: 1;
padding: $uni-spacing-col-base 0;
font-size: $uni-font-size-lg;
border: none;
outline: none;
background: transparent;
color: $uni-text-color;
&::placeholder {
color: $uni-text-color-placeholder;
}
}
/* 分组标题 */
.section-header {
display: flex;
align-items: center;
gap: 8px;
}
.section-icon {
font-size: $uni-font-size-lg;
}
/* 服务网格动画 */
.service-item {
animation-delay: calc(var(--item-index, 0) * 0.1s);
animation: slideInUp 0.6s ease-out backwards;
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 搜索焦点效果 */
.search-bar:focus-within {
transform: translateY(-2px);
box-shadow: $uni-shadow-hover;
.search-icon {
color: $uni-color-primary;
}
}
/* 底部间距 */
.bottom-spacer {
height: 20px;
}
/* 页面进入动画 */
.page-container {
animation: fadeInUp 0.6s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */
@media (max-width: 375px) {
.search-section {
padding: $uni-spacing-col-base;
}
.search-bar {
padding: 0 $uni-spacing-row-base;
}
.search-input {
font-size: $uni-font-size-base;
}
}
</style>
+177
View File
@@ -73,3 +73,180 @@
.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");
}
/* 新增常用图标 - 2025-01-07 */
/* 星星图标 */
.icon-star {
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 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z'/%3E%3C/svg%3E");
}
/* 心形图标 */
.icon-heart {
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 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'/%3E%3C/svg%3E");
}
/* 日历图标 */
.icon-calendar {
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 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z'/%3E%3C/svg%3E");
}
/* 聊天图标 */
.icon-chat {
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-2 .9-2 2v12c0 1.1.9 2 2 2h4l4 4 4-4h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z'/%3E%3C/svg%3E");
}
/* 工具图标 */
.icon-tools {
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='M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z'/%3E%3C/svg%3E");
}
/* 地图图标 */
.icon-map {
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.5 3l-.16.03L15 5.1 9 3 3.36 4.9c-.21.07-.36.25-.36.48V20.5c0 .28.22.5.5.5l.16-.03L9 18.9l6 2.1 5.64-1.9c.21-.07.36-.25.36-.48V3.5c0-.28-.22-.5-.5-.5zM15 19l-6-2.11V5l6 2.11V19z'/%3E%3C/svg%3E");
}
/* 播放圆圈图标 */
.icon-play-circle {
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 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z'/%3E%3C/svg%3E");
}
/* 列表图标 */
.icon-list {
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='M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z'/%3E%3C/svg%3E");
}
/* 人民币圆圈图标 */
.icon-rmb-circle {
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 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.5 6L12 10.5 10.5 8H9.5l1.75 2.5H9v1h3v1H9v1h3V15h1v-1.5h3v-1h-3v-1h3v-1h-2.25L14.5 8h-1z'/%3E%3C/svg%3E");
}
/* 商店图标 */
.icon-shop {
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 2l3.09 6.26L22 9.27 16.18 14l1.64 7L12 17.77 6.18 21l1.64-7L2 9.27l6.91-1.01L12 2zm0 0L4 8v12h16V8l-8-6z'/%3E%3C/svg%3E");
}
/* 奖杯图标 */
.icon-trophy {
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='M7 4V2c0-1.1.9-2 2-2h6c1.1 0 2 .9 2 2v2h4c1.1 0 2 .9 2 2v4c0 2.21-1.79 4-4 4h-1.28c-.44 2.83-2.82 5-5.72 5s-5.28-2.17-5.72-5H5c-2.21 0-4-1.79-4-4V6c0-1.1.9-2 2-2h4zM9 4h6V2H9v2zM5 6v4c0 1.1.9 2 2 2h1.28c.42-1.82 1.89-3.26 3.72-3.26s3.3 1.44 3.72 3.26H17c1.1 0 2-.9 2-2V6H5zm7 14c1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3 1.34 3 3 3z'/%3E%3C/svg%3E");
}
/* 朋友图标 */
.icon-friends {
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='M16 4c0-1.11.89-2 2-2s2 .89 2 2-.89 2-2 2-2-.89-2-2zm4 18v-6h2.5l-2.54-7.63A1.495 1.495 0 0 0 18.5 7.5h-3c-.57 0-1.08.32-1.34.82L11.5 15h2.5v7h6zM12.5 11.5c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5S11 9.17 11 10s.67 1.5 1.5 1.5zM5.5 6c1.11 0 2-.89 2-2s-.89-2-2-2-2 .89-2 2 .89 2 2 2zm2 16v-7H9l-1.46-3.12A1.495 1.495 0 0 0 6.12 11h-1.5c-.57 0-1.08.32-1.34.82L1 17h2.5v7h4z'/%3E%3C/svg%3E");
}
/* 飞机图标 */
.icon-plane {
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='M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z'/%3E%3C/svg%3E");
}
/* 书签图标 */
.icon-bookmark {
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='M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2z'/%3E%3C/svg%3E");
}
/* 人民币圆圈填充图标 */
.icon-rmb-circle-fill {
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 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.5 6L12 10.5 10.5 8H9.5l1.75 2.5H9v1h3v1H9v1h3V15h1v-1.5h3v-1h-3v-1h3v-1h-2.25L14.5 8h-1z'/%3E%3C/svg%3E");
}
/* 金钱圆圈图标 */
.icon-money-circle {
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.8 10.9c-2.27-.59-3-1.2-3-2.15 0-1.09 1.01-1.85 2.7-1.85 1.78 0 2.44.85 2.5 2.1h2.21c-.07-1.72-1.12-3.3-3.21-3.81V3h-3v2.16c-1.94.42-3.5 1.68-3.5 3.61 0 2.31 1.91 3.46 4.7 4.13 2.5.6 3 1.48 3 2.41 0 .69-.49 1.79-2.7 1.79-2.06 0-2.87-.92-2.98-2.1h-2.2c.12 2.19 1.76 3.42 3.68 3.83V21h3v-2.15c1.95-.37 3.5-1.5 3.5-3.55 0-2.84-2.43-3.81-4.7-4.4z'/%3E%3C/svg%3E");
}
/* 文件文本图标 */
.icon-file-text {
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='M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z'/%3E%3C/svg%3E");
}
/* 订单图标 */
.icon-order {
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 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z'/%3E%3C/svg%3E");
}
/* 设置填充图标 */
.icon-setting-fill {
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 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm8.94 3A8.994 8.994 0 0 0 13 3.06V1h-2v2.06A8.994 8.994 0 0 0 3.06 11H1v2h2.06A8.994 8.994 0 0 0 11 20.94V23h2v-2.06A8.994 8.994 0 0 0 20.94 13H23v-2h-2.06zM12 19c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z'/%3E%3C/svg%3E");
}
/* 用户圆圈填充图标 */
.icon-account-circle-fill {
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 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z'/%3E%3C/svg%3E");
}
/* 列表点图标 */
.icon-list-dot {
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='M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z'/%3E%3C/svg%3E");
}
/* 首页填充图标 */
.icon-home-fill {
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");
}
/* WiFi图标 */
.icon-wifi {
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='M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.07 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z'/%3E%3C/svg%3E");
}
/* 分享图标 */
.icon-share {
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 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z'/%3E%3C/svg%3E");
}
/* 分享方形图标 */
.icon-share-square {
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 2h-3c-1.1 0-2 .9-2 2v3H7c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2v-6h3c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-8 15H7v-3h3v3zm0-5H7v-2h3v2zm0-4H7V6h3v2zm5 9h-3v-3h3v3zm0-5h-3v-2h3v2zm0-4h-3V6h3v2zm5-3h-3V4h3v1z'/%3E%3C/svg%3E");
}
/* 编辑笔图标 */
.icon-edit-pen {
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='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E");
}
/* 时钟图标 */
.icon-clock {
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.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z'/%3E%3C/path%3E%3Cpath d='m12.5 7-1 1v4.25l3.5 2.08.75-1.23-2.75-1.63V8z'/%3E%3C/svg%3E");
}
/* 确认圆圈图标 */
.icon-checkmark-circle {
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 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z'/%3E%3C/svg%3E");
}
/* 复制图标 */
.icon-copy {
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='M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z'/%3E%3C/svg%3E");
}
/* 用户圆圈图标 */
.icon-account-circle {
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 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z'/%3E%3C/svg%3E");
}
/* 用户填充图标 */
.icon-account-fill {
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-star-fill {
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 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'/%3E%3C/svg%3E");
}
/* 包图标 */
.icon-bag {
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='M17 6h-2V3c0-.55-.45-1-1-1h-4c-.55 0-1 .45-1 1v3H7c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zM9.5 3h5v3h-5V3zM9 12v-2h2v2h2v-2h2v2c0 1.1-.9 2-2 2h-2c-1.1 0-2-.9-2-2z'/%3E%3C/svg%3E");
}
/* 学工管理填充图标 */
.icon-account-fill {
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-logout {
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='M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.59L17 17l5-5-5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z'/%3E%3C/svg%3E");
}
@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="7" r="4" fill="rgb(93, 135, 255)"/>
<path d="M12 14c-4.42 0-8 3.58-8 8h16c0-4.42-3.58-8-8-8z" fill="rgb(93, 135, 255)"/>
</svg>

Before

Width:  |  Height:  |  Size: 256 B

-5
View File
@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="7" r="4" fill="#999999"/>
<path d="M12 14c-4.42 0-8 3.58-8 8h16c0-4.42-3.58-8-8-8z" fill="#999999"/>
</svg>

Before

Width:  |  Height:  |  Size: 236 B

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M4 4h16v2H4V4zm0 4h16v2H4V8zm0 4h16v2H4v-2zm0 4h10v2H4v-2z" fill="rgb(93, 135, 255)"/>
<rect x="3" y="3" width="18" height="18" rx="2" stroke="rgb(93, 135, 255)" stroke-width="1.5" fill="none"/>
</svg>

Before

Width:  |  Height:  |  Size: 319 B

-5
View File
@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M4 4h16v2H4V4zm0 4h16v2H4V8zm0 4h16v2H4v-2zm0 4h10v2H4v-2z" fill="#999999"/>
<rect x="3" y="3" width="18" height="18" rx="2" stroke="#999999" stroke-width="1.5" fill="none"/>
</svg>

Before

Width:  |  Height:  |  Size: 299 B

+378
View File
@@ -0,0 +1,378 @@
# 移动端工具库使用说明
本文档说明移动端 `utils` 目录下各个工具的使用方法,特别是升级后的请求工具。
## 📁 文件结构
```
mobile/src/utils/
├── request.js # 统一请求工具(已升级)
├── crypto.js # 加密解密工具(新增)
├── config.js # 配置管理工具(新增)
├── encrypt-pure.js # 登录专用加密工具(保留)
├── encrypt-*.js # 其他加密实现(保留)
└── README.md # 本文档
```
## 🚀 核心升级功能
### 1. 统一的加密解密支持
移动端现在与 Web 端保持一致,支持:
- ✅ **入参加密**:通过 `isEncrypt: 'true'` 头部控制
- ✅ **出参解密**:自动处理后端返回的加密数据
- ✅ **AES + RSA 双重加密**:与后端完全兼容
- ✅ **防重复提交**:避免用户误操作
- ✅ **错误处理优化**:404静默处理、401自动跳转
### 2. 可配置的参数管理
所有关键参数都可以通过配置文件统一管理,方便调试和部署。
## 📖 使用方法
### 基础请求
```javascript
import { request } from '@/utils/request.js';
// 普通GET请求
const response = await request({
url: '/api/user/list',
method: 'GET'
});
// 普通POST请求
const response = await request({
url: '/api/user/create',
method: 'POST',
data: {
name: '张三',
age: 25
}
});
```
### 加密请求
```javascript
import { request } from '@/utils/request.js';
// 启用加密的POST请求
const response = await request({
url: '/api/sensitive/data',
method: 'POST',
data: {
sensitiveInfo: '敏感数据'
},
header: {
isEncrypt: 'true' // 关键:启用加密
}
});
```
### 免Token请求
```javascript
import { request } from '@/utils/request.js';
// 不需要Token的请求(如获取租户列表)
const response = await request({
url: '/auth/tenant/list',
method: 'GET',
header: {
isToken: false // 关键:不携带Token
}
});
```
### 允许重复提交
```javascript
import { request } from '@/utils/request.js';
// 允许重复提交的请求
const response = await request({
url: '/api/data/sync',
method: 'POST',
data: { syncData: 'xxx' },
header: {
repeatSubmit: false // 关键:允许重复提交
}
});
```
## ⚙️ 配置管理
### 基础配置更新
```javascript
import { updateConfig } from '@/utils/config.js';
// 更新API基础URL
updateConfig({
request: {
baseURL: {
production: 'https://new-api-domain.com'
}
}
});
// 更新客户端ID
updateConfig({
app: {
clientId: 'new-client-id-here'
}
});
```
### 加密配置更新
```javascript
import { updateConfig } from '@/utils/config.js';
// 更新RSA公钥
updateConfig({
encryption: {
rsaPublicKey: 'new-rsa-public-key-here'
}
});
// 临时禁用加密(调试用)
updateConfig({
encryption: {
enabled: false
}
});
```
### 预设配置方案
```javascript
import { applyPreset } from '@/utils/config.js';
// 应用生产环境配置
applyPreset('production');
// 应用测试环境配置
applyPreset('test');
// 应用开发环境配置
applyPreset('development');
```
### 单个配置项更新
```javascript
import { setConfigItem, getConfigItem } from '@/utils/config.js';
// 设置超时时间
setConfigItem('request.timeout', 600000); // 10分钟
// 获取当前客户端ID
const clientId = getConfigItem('app.clientId');
// 检查是否启用加密
const isEncrypted = getConfigItem('encryption.enabled');
```
## 🔐 加密工具直接使用
```javascript
import {
encryptWithAES,
decryptWithAES,
generateRandomAESKey,
encrypt,
decrypt
} from '@/utils/crypto.js';
// AES加密解密
const aesKey = generateRandomAESKey();
const encryptedData = encryptWithAES('敏感数据', aesKey);
const decryptedData = decryptWithAES(encryptedData, aesKey);
// RSA加密解密
const encryptedKey = encrypt('AES密钥');
const decryptedKey = decrypt(encryptedKey);
```
## 📦 API方法封装
### 登录
```javascript
import { api } from '@/utils/request.js';
// 用户登录(自动加密)
const loginResult = await api.login({
username: 'admin',
password: 'password123',
tenantId: '000000'
});
```
### 获取用户信息
```javascript
import { api } from '@/utils/request.js';
// 获取当前用户信息
const userInfo = await api.getUserInfo();
```
### 注销登录
```javascript
import { api } from '@/utils/request.js';
// 用户注销
await api.logout();
```
## 💾 缓存工具
### 用户信息缓存
```javascript
import { userStorage } from '@/utils/request.js';
// 保存登录信息
userStorage.saveLoginInfo(loginResult, userInfo);
// 获取用户信息
const user = userStorage.getUserInfo();
// 检查登录状态
const isLoggedIn = userStorage.isLoggedIn();
// 清除登录信息
userStorage.clearLoginInfo();
```
### 通用缓存
```javascript
import { storage } from '@/utils/request.js';
// 设置缓存(30分钟过期)
storage.set('cacheKey', { data: 'value' }, 1800);
// 获取缓存
const data = storage.get('cacheKey');
// 删除缓存
storage.remove('cacheKey');
// 清空所有缓存
storage.clear();
```
## 📥 文件下载
```javascript
import { download } from '@/utils/request.js';
// 下载文件
await download('/api/export/users', {
filters: { status: 'active' }
}, 'users.xlsx');
```
## 🔧 调试和日志
### 开启详细日志
```javascript
import { updateConfig } from '@/utils/config.js';
// 开启所有调试日志
updateConfig({
debug: {
enableVerboseLog: true,
showRequestLog: true,
showResponseLog: true,
showCryptoLog: true
}
});
```
### 错误处理配置
```javascript
import { updateConfig } from '@/utils/config.js';
// 自定义错误处理
updateConfig({
error: {
silentCodes: [404, 403], // 静默处理的状态码
toastDuration: 3000, // 错误提示显示时间
loginRedirectDelay: 1000 // 跳转登录延迟
}
});
```
## 🚨 注意事项
### 1. 加密兼容性
- 移动端加密算法与 Web 端完全一致
- RSA 公钥必须与后端保持同步
- 加密只对 POST/PUT 请求生效
### 2. 配置优先级
- 运行时配置 > 环境预设 > 默认配置
- 配置更新会立即生效,无需重启应用
### 3. Token 管理
- Token 自动从本地存储获取
- 401 错误会自动清除 Token 并跳转登录
- 支持 Token 过期时间管理
### 4. 错误处理
- 404 错误默认静默处理
- 403 错误会显示提示但不跳转
- 网络错误会显示友好提示
## 📊 性能优化
### 1. 防重复提交
- 默认 500ms 间隔防重复提交
- 可通过配置调整间隔时间
- 支持单个请求禁用防重复
### 2. 请求超时
- 普通请求:5分钟超时
- 下载请求:20分钟超时
- 可通过配置调整超时时间
### 3. 缓存策略
- 会话级缓存用于防重复提交
- 持久化缓存支持过期时间
- 登录信息缓存支持时间戳
## 🔗 与 Web 端对比
| 功能 | Web 端 | 移动端 | 说明 |
|------|--------|--------|------|
| 入参加密 | ✅ | ✅ | 完全一致 |
| 出参解密 | ✅ | ✅ | 完全一致 |
| 防重复提交 | ✅ | ✅ | 完全一致 |
| 401处理 | ✅ | ✅ | 跳转方式略有不同 |
| 404静默 | ✅ | ✅ | 完全一致 |
| 下载支持 | ✅ | ✅ | 平台实现不同 |
| 配置管理 | 环境变量 | 配置文件 | 更灵活的配置方式 |
## 🎯 最佳实践
1. **生产环境**:关闭调试日志,启用加密
2. **开发环境**:开启详细日志,保持加密测试
3. **配置管理**:使用预设配置,避免硬编码
4. **错误处理**:合理设置静默错误码
5. **性能优化**:适当设置超时和缓存策略
---
**📝 更新日志**
- 2025-01-07:完成移动端请求工具升级,与 Web 端功能对齐
- 支持入参加密、出参解密、防重复提交等完整功能
- 新增配置管理系统,提升可维护性
-2
View File
@@ -373,8 +373,6 @@ class AccessibilityUtils {
uni.switchTab = (options) => {
const pageTitles = {
'/pages/index/index': '智慧校园 - 首页',
'/pages/workbench/index': '智慧校园 - 办公中心',
'/pages/profile/index': '智慧校园 - 我的',
};
const title = pageTitles[options.url];
+353
View File
@@ -0,0 +1,353 @@
// 移动端配置文件 - 集中管理所有可配置参数
// 使用方法:import { updateConfig } from '@/utils/config.js' 然后调用 updateConfig(newConfig)
// 默认配置
const DEFAULT_CONFIG = {
// =========================
// 加密相关配置
// =========================
encryption: {
// 是否启用加密
enabled: true,
// RSA公钥 - 与后端保持一致
rsaPublicKey:
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
// RSA私钥 - 前端不建议存放,仅用于测试环境
rsaPrivateKey: '',
// 加密请求头名称
encryptHeader: 'encrypt-key',
// AES密钥长度
aesKeyLength: 32,
},
// =========================
// 请求相关配置
// =========================
request: {
// API基础URL
baseURL: {
development: '/api',
production: 'https://your-api-domain.com',
},
// 请求超时时间(毫秒)
timeout: 300000, // 5分钟
// 下载超时时间(毫秒)
downloadTimeout: 1200000, // 20分钟
// 防重复提交间隔时间(毫秒)
repeatSubmitInterval: 500,
// 默认请求头
defaultHeaders: {
'Content-Type': 'application/json',
},
},
// =========================
// 应用相关配置
// =========================
app: {
// 客户端ID
clientId: 'e5cd7e4891bf95d1d19206ce24a7b32e',
// 租户ID
tenantId: '000000',
// 默认referer
referer: 'http://localhost/login?redirect=/index',
// 登录页面路径
loginPath: '/pages/login/index',
},
// =========================
// 缓存相关配置
// =========================
cache: {
// 默认缓存过期时间(秒)
defaultExpire: 3600, // 1小时
// Token存储键名
tokenKey: 'access_token',
// 用户信息存储键名
userInfoKey: 'userInfo',
// 登录状态存储键名
loginStatusKey: 'isLoggedIn',
// 会话缓存前缀
sessionPrefix: 'session_',
},
// =========================
// 错误处理配置
// =========================
error: {
// 静默处理的HTTP状态码
silentCodes: [404],
// 自动跳转登录的状态码
loginRedirectCodes: [401],
// 错误提示显示时间(毫秒)
toastDuration: 2000,
// 跳转登录延迟时间(毫秒)
loginRedirectDelay: 500,
},
// =========================
// 调试相关配置
// =========================
debug: {
// 是否启用详细日志
enableVerboseLog: process.env.NODE_ENV === 'development',
// 是否显示请求日志
showRequestLog: true,
// 是否显示响应日志
showResponseLog: true,
// 是否显示加密解密日志
showCryptoLog: true,
},
};
// 当前配置 - 可被动态修改
let currentConfig = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
// =========================
// 配置管理方法
// =========================
/**
* 获取完整配置
* @returns {Object} 当前配置对象
*/
export const getConfig = () => {
return currentConfig;
};
/**
* 获取特定配置项
* @param {string} path 配置路径,支持嵌套 如: 'encryption.enabled' 或 'request.timeout'
* @returns {any} 配置值
*/
export const getConfigItem = (path) => {
const keys = path.split('.');
let value = currentConfig;
for (const key of keys) {
if (value && typeof value === 'object' && key in value) {
value = value[key];
} else {
console.warn(`配置项 ${path} 不存在`);
return undefined;
}
}
return value;
};
/**
* 更新配置
* @param {Object} newConfig 新配置对象(部分更新)
* @param {boolean} deep 是否深度合并,默认true
*/
export const updateConfig = (newConfig, deep = true) => {
if (deep) {
currentConfig = deepMerge(currentConfig, newConfig);
} else {
currentConfig = { ...currentConfig, ...newConfig };
}
console.log('🔧 配置已更新:', currentConfig);
// 通知其他模块配置已更新
notifyConfigUpdate();
};
/**
* 设置特定配置项
* @param {string} path 配置路径
* @param {any} value 配置值
*/
export const setConfigItem = (path, value) => {
const keys = path.split('.');
let target = currentConfig;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!target[key] || typeof target[key] !== 'object') {
target[key] = {};
}
target = target[key];
}
const lastKey = keys[keys.length - 1];
target[lastKey] = value;
console.log(`🔧 配置项 ${path} 已更新为:`, value);
// 通知其他模块配置已更新
notifyConfigUpdate();
};
/**
* 重置配置为默认值
*/
export const resetConfig = () => {
currentConfig = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
console.log('🔄 配置已重置为默认值');
// 通知其他模块配置已更新
notifyConfigUpdate();
};
/**
* 获取当前环境的API基础URL
* @returns {string} API基础URL
*/
export const getBaseURL = () => {
const env = process.env.NODE_ENV || 'development';
return currentConfig.request.baseURL[env] || currentConfig.request.baseURL.development;
};
// =========================
// 配置预设方案
// =========================
/**
* 开发环境配置
*/
export const developmentConfig = {
encryption: {
enabled: true, // 开发环境也启用加密测试
},
debug: {
enableVerboseLog: true,
showRequestLog: true,
showResponseLog: true,
showCryptoLog: true,
},
error: {
toastDuration: 3000, // 开发环境错误提示显示久一点
},
};
/**
* 生产环境配置
*/
export const productionConfig = {
encryption: {
enabled: true,
},
debug: {
enableVerboseLog: false,
showRequestLog: false,
showResponseLog: false,
showCryptoLog: false,
},
request: {
timeout: 300000, // 生产环境保持较长超时
repeatSubmitInterval: 1000, // 生产环境防重复提交间隔更长
},
};
/**
* 测试环境配置
*/
export const testConfig = {
encryption: {
enabled: true,
},
debug: {
enableVerboseLog: true,
showRequestLog: true,
showResponseLog: false,
showCryptoLog: true,
},
};
/**
* 应用预设配置
* @param {string} preset 预设名称: 'development' | 'production' | 'test'
*/
export const applyPreset = (preset) => {
const presets = {
development: developmentConfig,
production: productionConfig,
test: testConfig,
};
if (presets[preset]) {
updateConfig(presets[preset]);
console.log(`✅ 已应用 ${preset} 环境配置预设`);
} else {
console.error(`❌ 未知的配置预设: ${preset}`);
}
};
// =========================
// 工具方法
// =========================
/**
* 深度合并对象
* @param {Object} target 目标对象
* @param {Object} source 源对象
* @returns {Object} 合并后的对象
*/
const deepMerge = (target, source) => {
const result = { ...target };
for (const key in source) {
if (source.hasOwnProperty(key)) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = deepMerge(result[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
}
return result;
};
/**
* 通知其他模块配置已更新
*/
const notifyConfigUpdate = () => {
// 这里可以实现事件通知机制
// 例如使用 uni.$emit('configUpdated', currentConfig)
if (typeof uni !== 'undefined' && uni.$emit) {
uni.$emit('configUpdated', currentConfig);
}
};
// =========================
// 初始化配置
// =========================
// 根据环境自动应用配置预设
const env = process.env.NODE_ENV || 'development';
if (env !== 'development') {
applyPreset(env);
}
// 导出常用配置项的快捷访问方法
export const isEncryptionEnabled = () => getConfigItem('encryption.enabled');
export const getClientId = () => getConfigItem('app.clientId');
export const getTenantId = () => getConfigItem('app.tenantId');
export const getTimeout = () => getConfigItem('request.timeout');
export const isDebugMode = () => getConfigItem('debug.enableVerboseLog');
console.log('📋 移动端配置初始化完成,当前环境:', env);
+291
View File
@@ -0,0 +1,291 @@
// 移动端加密解密工具 - 与web端保持一致
import CryptoJS from 'crypto-js';
import JSEncrypt from 'jsencrypt';
// 加密配置 - 可配置
export const ENCRYPT_CONFIG = {
// RSA公钥 - 与后端保持一致
RSA_PUBLIC_KEY:
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
// RSA私钥 - 与web端配置文档保持一致
RSA_PRIVATE_KEY:
'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=',
// 客户端ID
CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
// 租户ID
TENANT_ID: '000000',
// 加密请求头
ENCRYPT_HEADER: 'encrypt-key',
// 是否启用加密
ENCRYPT_ENABLED: true,
};
/**
* 随机生成32位的字符串
* @returns {string}
*/
const generateRandomString = () => {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < 32; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
/**
* 随机生成AES密钥
* @returns {CryptoJS.lib.WordArray}
*/
export const generateAesKey = () => {
return CryptoJS.enc.Utf8.parse(generateRandomString());
};
/**
* 加密base64
* @param {CryptoJS.lib.WordArray} str
* @returns {string}
*/
export const encryptBase64 = (str) => {
return CryptoJS.enc.Base64.stringify(str);
};
/**
* 解密base64
* @param {string} str
* @returns {CryptoJS.lib.WordArray}
*/
export const decryptBase64 = (str) => {
return CryptoJS.enc.Base64.parse(str);
};
/**
* 使用密钥对数据进行AES加密
* @param {string} message 待加密的数据
* @param {CryptoJS.lib.WordArray} aesKey AES密钥
* @returns {string} 加密后的数据
*/
export const encryptWithAes = (message, aesKey) => {
const encrypted = CryptoJS.AES.encrypt(message, aesKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.toString();
};
/**
* 使用密钥对数据进行AES解密
* @param {string} message 待解密的数据
* @param {CryptoJS.lib.WordArray} aesKey AES密钥
* @returns {string} 解密后的数据
*/
export const decryptWithAes = (message, aesKey) => {
const decrypted = CryptoJS.AES.decrypt(message, aesKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return decrypted.toString(CryptoJS.enc.Utf8);
};
/**
* RSA加密
* @param {string} txt 待加密的文本
* @returns {string|false} 加密后的数据
*/
export const encrypt = (txt) => {
try {
console.log('🔐 开始RSA加密,输入文本长度:', txt ? txt.length : 'null');
console.log('🔐 使用的RSA公钥:', ENCRYPT_CONFIG.RSA_PUBLIC_KEY.substring(0, 20) + '...');
const encryptor = new JSEncrypt();
// 构造完整的PEM格式RSA公钥
const publicKey = `-----BEGIN PUBLIC KEY-----
${ENCRYPT_CONFIG.RSA_PUBLIC_KEY}
-----END PUBLIC KEY-----`;
console.log('🔐 构造的完整公钥:', publicKey.substring(0, 50) + '...');
encryptor.setPublicKey(publicKey);
console.log('🔐 公钥设置完成');
const result = encryptor.encrypt(txt);
if (result) {
console.log('✅ RSA加密成功,结果长度:', result.length);
console.log('✅ 加密结果前50位:', result.substring(0, 50) + '...');
} else {
console.error('❌ RSA加密返回空结果');
}
return result;
} catch (error) {
console.error('❌ RSA加密失败:', error);
console.error('❌ 错误堆栈:', error.stack);
return false;
}
};
/**
* RSA解密
* @param {string} txt 待解密的文本
* @returns {string|false} 解密后的数据
*/
export const decrypt = (txt) => {
try {
console.log('🔓 开始RSA解密,输入文本长度:', txt ? txt.length : 'null');
console.log('🔓 使用的RSA私钥:', ENCRYPT_CONFIG.RSA_PRIVATE_KEY.substring(0, 20) + '...');
if (!ENCRYPT_CONFIG.RSA_PRIVATE_KEY) {
console.warn('❌ RSA私钥未配置,无法解密');
return false;
}
const encryptor = new JSEncrypt();
// 构造完整的PEM格式RSA私钥
const privateKey = `-----BEGIN PRIVATE KEY-----
${ENCRYPT_CONFIG.RSA_PRIVATE_KEY}
-----END PRIVATE KEY-----`;
console.log('🔓 构造的完整私钥:', privateKey.substring(0, 50) + '...');
encryptor.setPrivateKey(privateKey);
console.log('🔓 私钥设置完成');
const result = encryptor.decrypt(txt);
if (result) {
console.log('✅ RSA解密成功,结果长度:', result.length);
console.log('✅ 解密结果前50位:', result.substring(0, 50) + '...');
} else {
console.error('❌ RSA解密返回空结果');
}
return result;
} catch (error) {
console.error('❌ RSA解密失败:', error);
console.error('❌ 错误堆栈:', error.stack);
return false;
}
};
/**
* 生成随机字符串AES密钥(字符串格式)
* @param {number} length 密钥长度,默认32位
* @returns {string} AES密钥字符串
*/
export const generateRandomAESKey = (length = 32) => {
return generateRandomString();
};
/**
* 使用字符串密钥进行AES加密(兼容原有函数)
* @param {string} data 待加密的数据
* @param {string} key AES密钥字符串
* @returns {string} 加密后的数据
*/
export const 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();
};
/**
* 使用字符串密钥进行AES解密(兼容原有函数)
* @param {string} data 待解密的数据
* @param {string} key AES密钥字符串
* @returns {string} 解密后的数据
*/
export const decryptWithAES = (data, key) => {
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
const decrypted = CryptoJS.AES.decrypt(data, keyUtf8, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return decrypted.toString(CryptoJS.enc.Utf8);
};
/**
* 更新加密配置
* @param {Object} config 新的配置对象
*/
export const updateEncryptConfig = (config) => {
Object.assign(ENCRYPT_CONFIG, config);
console.log('🔧 加密配置已更新:', ENCRYPT_CONFIG);
};
/**
* 验证RSA密钥对配置
* @returns {boolean} 验证结果
*/
export const validateKeyPair = () => {
try {
console.log('🔍 开始验证RSA密钥对配置...');
// 检查公钥和私钥是否存在
if (!ENCRYPT_CONFIG.RSA_PUBLIC_KEY) {
console.error('❌ RSA公钥未配置');
return false;
}
if (!ENCRYPT_CONFIG.RSA_PRIVATE_KEY) {
console.error('❌ RSA私钥未配置');
return false;
}
// 测试加密解密功能
const testMessage = 'RSA密钥对验证测试消息';
console.log('🧪 测试消息:', testMessage);
// 使用公钥加密
const encrypted = encrypt(testMessage);
if (!encrypted) {
console.error('❌ RSA加密测试失败');
return false;
}
console.log('✅ RSA加密测试成功');
// 使用私钥解密
const decrypted = decrypt(encrypted);
if (!decrypted || decrypted !== testMessage) {
console.error('❌ RSA解密测试失败');
console.error('原始消息:', testMessage);
console.error('解密结果:', decrypted);
return false;
}
console.log('✅ RSA解密测试成功');
console.log('🎉 RSA密钥对配置验证通过!');
return true;
} catch (error) {
console.error('❌ RSA密钥对验证过程中出现错误:', error);
return false;
}
};
/**
* 获取当前加密配置摘要
* @returns {Object} 配置摘要
*/
export const getConfigSummary = () => {
return {
hasPublicKey: !!ENCRYPT_CONFIG.RSA_PUBLIC_KEY,
hasPrivateKey: !!ENCRYPT_CONFIG.RSA_PRIVATE_KEY,
publicKeyLength: ENCRYPT_CONFIG.RSA_PUBLIC_KEY ? ENCRYPT_CONFIG.RSA_PUBLIC_KEY.length : 0,
privateKeyLength: ENCRYPT_CONFIG.RSA_PRIVATE_KEY
? ENCRYPT_CONFIG.RSA_PRIVATE_KEY.length
: 0,
clientId: ENCRYPT_CONFIG.CLIENT_ID,
tenantId: ENCRYPT_CONFIG.TENANT_ID,
encryptHeader: ENCRYPT_CONFIG.ENCRYPT_HEADER,
encryptEnabled: ENCRYPT_CONFIG.ENCRYPT_ENABLED,
};
};
// 导出配置以便外部修改
export { ENCRYPT_CONFIG as config };
+7 -20
View File
@@ -375,7 +375,7 @@ class GestureUtils {
class PageSwipeGesture {
constructor() {
this.currentPageIndex = 0;
this.pages = ['home', 'workbench', 'profile'];
this.pages = ['home'];
this.isAnimating = false;
}
@@ -387,29 +387,18 @@ class PageSwipeGesture {
enableDoubleTap: false,
});
gesture.on('swipeleft', () => {
this.nextPage();
});
gesture.on('swiperight', () => {
this.prevPage();
});
// 只有一个页面,禁用滑动切换
return gesture;
}
nextPage() {
if (this.isAnimating) return;
this.currentPageIndex = (this.currentPageIndex + 1) % this.pages.length;
this.switchPage(this.pages[this.currentPageIndex]);
// 只有一个页面,不执行切换
return;
}
prevPage() {
if (this.isAnimating) return;
this.currentPageIndex = (this.currentPageIndex - 1 + this.pages.length) % this.pages.length;
this.switchPage(this.pages[this.currentPageIndex]);
// 只有一个页面,不执行切换
return;
}
switchPage(pageName) {
@@ -417,12 +406,10 @@ class PageSwipeGesture {
const pageUrls = {
home: '/pages/index/index',
workbench: '/pages/workbench/index',
profile: '/pages/profile/index',
};
if (pageUrls[pageName]) {
uni.switchTab({
uni.navigateTo({
url: pageUrls[pageName],
success: () => {
setTimeout(() => {
+192
View File
@@ -0,0 +1,192 @@
/**
* iconfont 图标管理工具
* 帮助开发者快速添加和管理自定义图标
*/
/**
* SVG Path 转换为 iconfont CSS 代码
* @param {string} iconName - 图标名称 (如: star, heart)
* @param {string} svgPath - SVG path 数据
* @param {string} viewBox - SVG viewBox, 默认 "0 0 24 24"
* @returns {string} CSS 代码
*/
export function generateIconCSS(iconName, svgPath, viewBox = '0 0 24 24') {
const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='${viewBox}' fill='currentColor'><path d='${svgPath}'/></svg>`;
const encodedSvg = encodeURIComponent(svg).replace(/'/g, '%27').replace(/"/g, '%22');
return `/* ${iconName}图标 */
.icon-${iconName} {
background-image: url("data:image/svg+xml;charset=utf8,${encodedSvg}");
}`;
}
/**
* 批量生成图标 CSS
* @param {Array} icons - 图标数组 [{name, path, viewBox}]
* @returns {string} 完整的 CSS 代码
*/
export function generateBatchIconCSS(icons) {
return icons.map((icon) => generateIconCSS(icon.name, icon.path, icon.viewBox)).join('\n\n');
}
/**
* 从完整SVG生成CSS
* @param {string} iconName - 图标名称
* @param {string} svgContent - 完整的SVG内容
* @returns {string} CSS 代码
*/
export function generateIconCSSFromSVG(iconName, svgContent) {
const encodedSvg = encodeURIComponent(svgContent).replace(/'/g, '%27').replace(/"/g, '%22');
return `/* ${iconName}图标 */
.icon-${iconName} {
background-image: url("data:image/svg+xml;charset=utf8,${encodedSvg}");
}`;
}
/**
* 预定义的常用图标路径
* 来源:Material Design Icons
*/
export const COMMON_ICONS = {
// 基础图标
user: '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',
lock: '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',
home: 'M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z',
setting:
'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',
// 操作图标
add: 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z',
close: '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',
edit: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z',
delete: 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z',
search: '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',
check: 'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z',
// 状态图标
success:
'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z',
warning: 'M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z',
info: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z',
error: 'M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z',
// 文件图标
download: 'M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z',
upload: 'M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z',
// 导航图标
arrowLeft: 'M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z',
arrowRight: 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z',
arrowUp: 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z',
arrowDown: 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z',
// 社交图标
star: 'M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z',
heart: 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z',
message:
'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',
phone: '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',
mail: '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',
};
/**
* 快速生成常用图标的CSS
* @param {string[]} iconNames - 图标名称数组
* @returns {string} CSS 代码
*/
export function generateCommonIconsCSS(iconNames) {
const selectedIcons = iconNames
.map((name) => ({
name,
path: COMMON_ICONS[name],
}))
.filter((icon) => icon.path);
return generateBatchIconCSS(selectedIcons);
}
/**
* 检查图标名称是否已存在
* @param {string} iconName - 图标名称
* @param {string} cssContent - 当前CSS文件内容
* @returns {boolean} 是否已存在
*/
export function checkIconExists(iconName, cssContent) {
return cssContent.includes(`.icon-${iconName}`);
}
/**
* 验证SVG内容
* @param {string} svgContent - SVG内容
* @returns {boolean} 是否有效
*/
export function validateSVG(svgContent) {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(svgContent, 'image/svg+xml');
return !doc.querySelector('parsererror');
} catch (error) {
return false;
}
}
/**
* 使用示例
*/
export const USAGE_EXAMPLES = {
// 单个图标生成
singleIcon: {
code: `import { generateIconCSS } from '@/utils/iconHelper';
const css = generateIconCSS(
'calendar',
'M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z'
);
console.log(css);`,
output: `/* calendar图标 */
.icon-calendar {
background-image: url("data:image/svg+xml;charset=utf8,...");
}`,
},
// 批量图标生成
batchIcons: {
code: `import { generateCommonIconsCSS } from '@/utils/iconHelper';
const css = generateCommonIconsCSS(['star', 'heart', 'message']);
console.log(css);`,
output: `/* star图标 */
.icon-star { ... }
/* heart图标 */
.icon-heart { ... }
/* message图标 */
.icon-message { ... }`,
},
// 从SVG生成
fromSVG: {
code: `import { generateIconCSSFromSVG } from '@/utils/iconHelper';
const svgContent = \`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"/>
</svg>\`;
const css = generateIconCSSFromSVG('circle', svgContent);
console.log(css);`,
},
};
// 导出所有工具函数
export default {
generateIconCSS,
generateBatchIconCSS,
generateIconCSSFromSVG,
generateCommonIconsCSS,
checkIconExists,
validateSVG,
COMMON_ICONS,
USAGE_EXAMPLES,
};
+122
View File
@@ -0,0 +1,122 @@
/**
* 移动端权限校验工具
* 参考web端的permission.ts实现
*/
import { userManager } from '@/utils/user'
/**
* 字符权限校验
* @param {Array} value 校验值
* @returns {Boolean}
*/
export const checkPermi = (value) => {
if (value && value instanceof Array && value.length > 0) {
const userInfo = userManager.getCurrentUser()
if (!userInfo || !userInfo.permissions) {
return false
}
const permissions = userInfo.permissions
const permissionDatas = value
const all_permission = '*:*:*'
const hasPermission = permissions.some((permission) => {
return all_permission === permission || permissionDatas.includes(permission)
})
if (!hasPermission) {
return false
}
return true
} else {
console.error('need roles! Like checkPermi="[\'system:user:add\',\'system:user:edit\']"')
return false
}
}
/**
* 角色权限校验
* @param {Array} value 校验值
* @returns {Boolean}
*/
export const checkRole = (value) => {
if (value && value instanceof Array && value.length > 0) {
const userInfo = userManager.getCurrentUser()
if (!userInfo || !userInfo.roles) {
return false
}
const roles = userInfo.roles
const permissionRoles = value
const super_admin = 'admin'
const hasRole = roles.some((role) => {
return super_admin === role || permissionRoles.includes(role)
})
if (!hasRole) {
return false
}
return true
} else {
console.error('need roles! Like checkRole="[\'admin\',\'editor\']"')
return false
}
}
/**
* 权限指令校验函数
* 用于在模板中进行权限判断
* @param {Array|String} permissions 权限标识数组或单个权限标识
* @returns {Boolean}
*/
export const hasPermission = (permissions) => {
if (!permissions) return true
// 如果传入的是字符串,转换为数组
const perms = Array.isArray(permissions) ? permissions : [permissions]
return checkPermi(perms)
}
/**
* Vue过滤器:权限校验
* 在模板中使用:v-if="hasPermi(['system:user:add'])"
*/
export const hasPermi = hasPermission
/**
* 用户角色校验
* @param {Array|String} roles 角色标识数组或单个角色标识
* @returns {Boolean}
*/
export const hasRole = (roles) => {
if (!roles) return true
// 如果传入的是字符串,转换为数组
const roleList = Array.isArray(roles) ? roles : [roles]
return checkRole(roleList)
}
/**
* 权限控制混入
* 在页面或组件中使用
*/
export const permissionMixin = {
methods: {
// 权限校验方法
hasPermi(permissions) {
return hasPermission(permissions)
},
// 角色校验方法
hasRole(roles) {
return hasRole(roles)
},
// 检查单个权限
checkPermission(permission) {
return hasPermission([permission])
},
},
}
+321 -98
View File
@@ -1,10 +1,66 @@
import { encryptLoginData } from './encrypt-pure.js';
import {
generateAesKey,
encryptBase64,
decryptBase64,
encryptWithAes,
decryptWithAes,
encrypt,
decrypt,
ENCRYPT_CONFIG,
updateEncryptConfig,
validateKeyPair,
getConfigSummary,
} from './crypto.js';
// API请求配置
const baseURL =
process.env.NODE_ENV === 'development'
? '/api' // 开发环境使用代理
: 'https://your-api-domain.com';
// API请求配置 - 可配置
const REQUEST_CONFIG = {
// 基础URL配置
baseURL: process.env.NODE_ENV === 'development' ? '/api' : 'https://your-api-domain.com',
// 请求超时时间
timeout: 300000, // 5分钟,适合大数据量导出
// 客户端ID
clientId: 'e5cd7e4891bf95d1d19206ce24a7b32e',
// 默认referer
referer: 'http://localhost/login?redirect=/index',
// 是否启用加密
encryptEnabled: true,
// 防重复提交间隔时间(ms)
repeatSubmitInterval: 500,
};
// 会话缓存工具
const sessionCache = {
set: (key, value) => {
try {
uni.setStorageSync(`session_${key}`, JSON.stringify(value));
} catch (e) {
console.error('设置会话缓存失败:', e);
}
},
get: (key) => {
try {
const data = uni.getStorageSync(`session_${key}`);
return data ? JSON.parse(data) : null;
} catch (e) {
console.error('获取会话缓存失败:', e);
return null;
}
},
remove: (key) => {
try {
uni.removeStorageSync(`session_${key}`);
} catch (e) {
console.error('删除会话缓存失败:', e);
}
},
};
// 统一请求方法
export const request = (options) => {
@@ -12,18 +68,157 @@ export const request = (options) => {
// 获取token
const token = getToken();
uni.request({
url: baseURL + options.url,
// 处理请求配置
const config = {
url: REQUEST_CONFIG.baseURL + options.url,
method: options.method || 'GET',
data: options.data || {},
timeout: options.timeout || REQUEST_CONFIG.timeout,
header: {
'Content-Type': 'application/json',
authorization: token,
clientid: 'e5cd7e4891bf95d1d19206ce24a7b32e',
referer: 'http://localhost/login?redirect=/index',
clientid: options.clientId || REQUEST_CONFIG.clientId,
// 注意:在H5环境中,浏览器会自动设置referer,手动设置会报错
// referer: options.referer || REQUEST_CONFIG.referer,
...options.header,
},
};
// 检查特殊header配置
const isToken = options.header?.isToken === false;
const isRepeatSubmit = options.header?.repeatSubmit === false;
const isEncrypt = options.header?.isEncrypt === 'true';
// 如果不需要token,移除authorization
if (isToken) {
delete config.header.authorization;
}
// 防重复提交检查
if (!isRepeatSubmit && (config.method === 'POST' || config.method === 'PUT')) {
const requestObj = {
url: config.url,
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
time: new Date().getTime(),
};
const sessionObj = sessionCache.get('sessionObj');
if (sessionObj) {
const s_url = sessionObj.url;
const s_data = sessionObj.data;
const s_time = sessionObj.time;
const interval = REQUEST_CONFIG.repeatSubmitInterval;
if (
s_data === requestObj.data &&
requestObj.time - s_time < interval &&
s_url === requestObj.url
) {
const message = '数据正在处理,请勿重复提交';
console.warn(`[${s_url}]: ` + message);
uni.showToast({
title: message,
icon: 'none',
});
return reject(new Error(message));
}
}
sessionCache.set('sessionObj', requestObj);
}
// 加密处理
if (
REQUEST_CONFIG.encryptEnabled &&
isEncrypt &&
(config.method === 'POST' || config.method === 'PUT')
) {
try {
console.log('🔐 开始请求加密处理...');
// 生成AES密钥
const aesKey = generateAesKey();
// 使用RSA加密AES密钥
const encryptedKey = encrypt(encryptBase64(aesKey));
if (!encryptedKey) {
throw new Error('RSA加密AES密钥失败');
}
// 设置加密头
config.header[ENCRYPT_CONFIG.ENCRYPT_HEADER] = encryptedKey;
// 加密请求数据
const dataToEncrypt =
typeof config.data === 'object' ? JSON.stringify(config.data) : config.data;
config.data = encryptWithAes(dataToEncrypt, aesKey);
console.log('✅ 请求数据加密完成');
} catch (error) {
console.error('❌ 请求加密失败:', error);
uni.showToast({
title: '请求加密失败',
icon: 'none',
});
return reject(error);
}
}
// FormData处理
if (config.data instanceof globalThis.FormData) {
delete config.header['Content-Type'];
}
console.log('📤 发送请求:', {
url: config.url,
method: config.method,
hasToken: !!token,
isEncrypt: isEncrypt,
});
// 发送请求
uni.request({
...config,
success: (res) => {
console.log('📥 收到响应:', {
statusCode: res.statusCode,
url: config.url,
hasEncryptHeader: !!res.header[ENCRYPT_CONFIG.ENCRYPT_HEADER],
});
// 解密处理
if (REQUEST_CONFIG.encryptEnabled && res.header[ENCRYPT_CONFIG.ENCRYPT_HEADER]) {
try {
console.log('🔓 开始响应解密处理...');
// 获取加密的AES密钥
const keyStr = res.header[ENCRYPT_CONFIG.ENCRYPT_HEADER];
// RSA解密获取AES密钥
const base64Str = decrypt(keyStr);
if (!base64Str) {
throw new Error('RSA解密AES密钥失败');
}
// Base64解码获取AES密钥
const aesKey = decryptBase64(base64Str);
// AES解密响应数据
const decryptedData = decryptWithAes(res.data, aesKey);
// 解析JSON
res.data = JSON.parse(decryptedData);
console.log('✅ 响应数据解密完成');
} catch (error) {
console.error('❌ 响应解密失败:', error);
uni.showToast({
title: '响应解密失败',
icon: 'none',
});
return reject(error);
}
}
// 处理响应
if (res.statusCode === 200) {
// 检查业务状态码
@@ -37,10 +232,14 @@ export const request = (options) => {
reject(res.data);
} else {
// 其他业务错误
uni.showToast({
title: res.data.msg || '请求失败',
icon: 'none',
});
const errorMsg = res.data.msg || '请求失败';
if (res.data.code !== 404) {
// 404错误静默处理
uni.showToast({
title: errorMsg,
icon: 'none',
});
}
reject(res.data);
}
} else {
@@ -51,18 +250,34 @@ export const request = (options) => {
console.log('🔒 HTTP 401未授权,自动跳转到登录页');
handleUnauthorized('登录已失效,请重新登录');
reject(res);
} else if (res.statusCode === 404) {
// 404错误静默处理
console.log('404错误被静默处理,不显示用户提示');
reject(res);
} else {
// 其他HTTP状态码错误
const errorMsg = `请求失败: ${res.statusCode}`;
uni.showToast({
title: `请求失败: ${res.statusCode}`,
title: errorMsg,
icon: 'none',
});
reject(res);
}
},
fail: (error) => {
console.error('❌ 网络请求失败:', error);
let message = '网络请求失败';
if (error.errMsg) {
if (error.errMsg.includes('timeout')) {
message = '请求超时,请检查网络连接';
} else if (error.errMsg.includes('fail')) {
message = '网络连接失败,请检查网络';
}
}
uni.showToast({
title: '网络请求失败',
title: message,
icon: 'none',
});
reject(error);
@@ -133,87 +348,25 @@ function handleUnauthorized(message = '登录已失效,请重新登录') {
// API方法封装
export const api = {
// 用户登录
login: async (data) => {
try {
console.log('🔐 开始加密登录流程...');
// 用户登录 - 使用统一的request方法和加密拦截器
login: (data) => {
console.log('🔐 开始登录流程...');
// 加密登录数据
const encryptResult = encryptLoginData({
return request({
url: '/auth/login',
method: 'POST',
data: {
username: data.username,
password: data.password,
tenantId: data.tenantId || '000000',
clientId: data.clientId || 'e5cd7e4891bf95d1d19206ce24a7b32e',
clientId: data.clientId || REQUEST_CONFIG.clientId,
grantType: data.grantType || 'password',
});
// 如果encryptLoginData返回的是Promise,需要await
const actualEncryptResult = encryptResult.then ? await encryptResult : encryptResult;
if (!actualEncryptResult.success) {
console.error('❌ 登录数据加密失败:', actualEncryptResult.error);
throw new Error(actualEncryptResult.error);
}
console.log('✅ 登录数据加密成功,发送加密请求...');
// 使用加密数据和自定义header发送请求
return new Promise((resolve, reject) => {
uni.request({
url: baseURL + '/auth/login',
method: 'POST',
data: actualEncryptResult.encryptedData, // 发送加密字符串,不是JSON对象
header: {
...actualEncryptResult.headers,
referer: 'http://localhost/login?redirect=/index', // 添加正确格式的referer
},
success: (res) => {
console.log('🔐 加密登录响应:', res);
if (res.statusCode === 200) {
if (res.data && res.data.code !== undefined) {
if (res.data.code === 200) {
console.log('✅ 加密登录成功');
resolve(res.data);
} else {
console.log('❌ 登录失败:', res.data.msg);
uni.showToast({
title: res.data.msg || '登录失败',
icon: 'none',
});
reject(res.data);
}
} else {
resolve(res.data);
}
} else {
console.log('❌ HTTP状态错误:', res.statusCode);
uni.showToast({
title: `请求失败: ${res.statusCode}`,
icon: 'none',
});
reject(res);
}
},
fail: (error) => {
console.log('❌ 登录请求失败:', error);
uni.showToast({
title: '网络请求失败',
icon: 'none',
});
reject(error);
},
});
});
} catch (error) {
console.error('❌ 登录加密处理失败:', error);
console.error('❌ 错误详情:', error.message);
console.error('❌ 错误堆栈:', error.stack);
uni.showToast({
title: '登录处理失败: ' + error.message,
icon: 'none',
});
return Promise.reject(error);
}
},
header: {
isToken: false, // 登录接口不需要token
isEncrypt: 'true', // 启用加密
},
});
},
// 获取用户信息
@@ -227,11 +380,6 @@ export const api = {
return request({
url: '/system/user/getInfo',
method: 'GET',
header: {
authorization: token,
clientid: 'e5cd7e4891bf95d1d19206ce24a7b32e',
referer: 'http://localhost/login?redirect=/index',
},
});
},
@@ -265,7 +413,7 @@ export const userStorage = {
if (userInfo) {
const enhancedUserInfo = {
...userInfo,
loginTime: new Date().toISOString(), // 记录登录时间
loginTime: new Date().toISOString(),
tokenExpireTime: loginResult.expires_in
? new Date(Date.now() + loginResult.expires_in * 1000).toISOString()
: null,
@@ -333,6 +481,26 @@ export const userStorage = {
console.error('清除登录信息失败:', e);
}
},
// 清除所有用户相关数据(包括refresh_token等)
clearAll: () => {
try {
// 清除所有登录相关的存储
uni.removeStorageSync('access_token');
uni.removeStorageSync('userInfo');
uni.removeStorageSync('isLoggedIn');
uni.removeStorageSync('refresh_token');
// 清除可能存在的其他用户相关数据
uni.removeStorageSync('userPermissions');
uni.removeStorageSync('userRoles');
uni.removeStorageSync('tenantInfo');
console.log('所有用户相关数据已清除');
} catch (e) {
console.error('清除所有用户数据失败:', e);
}
},
};
// 数据缓存工具
@@ -390,3 +558,58 @@ export const storage = {
}
},
};
// 通用下载方法
export const download = (url, params, fileName) => {
// 显示加载提示
uni.showLoading({
title: '正在下载数据,请稍候',
mask: true,
});
return request({
url: url,
method: 'POST',
data: params,
header: {
'Content-Type': 'application/x-www-form-urlencoded',
},
timeout: 1200000, // 20分钟超时
responseType: 'arraybuffer',
})
.then((resp) => {
// 处理下载响应
// 注意:uni-app中文件下载需要特殊处理
console.log('文件下载完成:', fileName);
uni.hideLoading();
// 这里需要根据平台进行不同的文件保存处理
// 具体实现可能需要平台特定的代码
})
.catch((error) => {
console.error('下载失败:', error);
uni.hideLoading();
uni.showToast({
title: '下载文件出现错误,请联系管理员!',
icon: 'none',
});
});
};
// 配置更新方法
export const updateRequestConfig = (config) => {
Object.assign(REQUEST_CONFIG, config);
console.log('🔧 请求配置已更新:', REQUEST_CONFIG);
};
// 加密配置更新方法
export const updateCryptoConfig = updateEncryptConfig;
// 密钥对验证方法
export const validateCryptoKeys = validateKeyPair;
// 获取加密配置摘要
export const getCryptoConfigSummary = getConfigSummary;
// 导出配置以便外部修改
export { REQUEST_CONFIG as config, ENCRYPT_CONFIG as cryptoConfig };
+1 -1
View File
@@ -176,7 +176,7 @@ export const permissionUtils = {
*/
checkPagePermission(pagePath) {
// 公开页面,无需权限检查
const publicPages = ['/pages/login/index', '/pages/index/index', '/pages/profile/index'];
const publicPages = ['/pages/login/index', '/pages/index/index'];
if (publicPages.includes(pagePath)) {
return true;