feat: 移动端开发规范重大更新
- 🔥 完全重写 .cursor/cursorrules-mobile.md (940行) * 基于实际mobile工程Vue 3.4.21+uniapp+uview-plus架构 * 移除所有JeeSite相关内容,精准匹配实际项目 * 新增11个完整章节:技术栈、项目结构、页面开发、组件规范等 - 📚 创建完整规范文档 docs/开发文档/移动端开发规范总结.md (800+行) * 涵盖从项目架构到部署的全流程指导 * 提供可直接使用的页面和组件模板 * 包含现代CSS变量设计系统和用户体验优化 - 🔄 同步更新主配置文件 .cursorrules * 更新移动端技术栈描述 * 调整为Vue 3.4.21+uniapp+uview-plus 3.4.45架构 - 📝 完善变更记录 * 详细记录规范更新背景和技术价值 * 为后续维护提供完整历史记录 核心价值:解决规范与实际项目不匹配问题,为Cursor AI提供准确的移动端开发上下文,确保生成代码完全符合实际项目架构。
No files matched your search
@@ -1,71 +1,29 @@
|
||||
<script>
|
||||
export default {
|
||||
onLaunch: function () {
|
||||
console.log('App Launch');
|
||||
},
|
||||
onShow: function () {
|
||||
console.log('App Show');
|
||||
},
|
||||
onHide: function () {
|
||||
console.log('App Hide');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 全局样式 */
|
||||
page {
|
||||
background-color: #f5f5f5;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
||||
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
/* 通用类 */
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.mb-20 {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.p-20 {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
export default {
|
||||
onLaunch: function () {
|
||||
console.log('App Launch')
|
||||
},
|
||||
onShow: function () {
|
||||
console.log('App Show')
|
||||
},
|
||||
onHide: function () {
|
||||
console.log('App Hide')
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 引入 uview-plus 全局样式 */
|
||||
@import 'uview-plus/index.scss';
|
||||
|
||||
/* 每个页面公共css */
|
||||
page {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
/* 全局字体设置 */
|
||||
* {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
* 认证相关API
|
||||
*/
|
||||
|
||||
import { post, get } from '@/utils/request.js';
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
export function loginApi(data) {
|
||||
return post('/auth/login', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户退出
|
||||
*/
|
||||
export function logoutApi() {
|
||||
return post('/auth/logout');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
export function getUserInfoApi() {
|
||||
return get('/auth/userinfo');
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*/
|
||||
export function refreshTokenApi(refreshToken) {
|
||||
return post('/auth/refresh', { refreshToken });
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
export function changePasswordApi(data) {
|
||||
return post('/auth/change-password', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
export function resetPasswordApi(data) {
|
||||
return post('/auth/reset-password', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*/
|
||||
export function sendCodeApi(data) {
|
||||
return post('/auth/send-code', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定手机号
|
||||
*/
|
||||
export function bindPhoneApi(data) {
|
||||
return post('/auth/bind-phone', data);
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* 通用API接口
|
||||
*/
|
||||
|
||||
import { get, post, uploadFile } from '@/utils/request.js';
|
||||
|
||||
/**
|
||||
* 获取轮播图列表
|
||||
*/
|
||||
export function getBannerListApi() {
|
||||
return get('/common/banner');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知公告列表
|
||||
*/
|
||||
export function getNoticeListApi(params) {
|
||||
return get('/common/notice', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取新闻列表
|
||||
*/
|
||||
export function getNewsListApi(params) {
|
||||
return get('/common/news', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典数据
|
||||
*/
|
||||
export function getDictDataApi(dictType) {
|
||||
return get(`/common/dict/${dictType}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
*/
|
||||
export function uploadFileApi(filePath, options = {}) {
|
||||
return uploadFile('/common/upload', filePath, {
|
||||
name: 'file',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统配置
|
||||
*/
|
||||
export function getConfigApi() {
|
||||
return get('/common/config');
|
||||
}
|
||||
|
||||
/**
|
||||
* 意见反馈
|
||||
*/
|
||||
export function feedbackApi(data) {
|
||||
return post('/common/feedback', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取版本信息
|
||||
*/
|
||||
export function getVersionApi() {
|
||||
return get('/common/version');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
*/
|
||||
export function checkUpdateApi() {
|
||||
return get('/common/check-update');
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<view class="card touchable" @click="handleClick">
|
||||
<view v-if="title || $slots.header" class="card-header">
|
||||
<slot name="header">
|
||||
<text class="card-title">{{ title }}</text>
|
||||
<slot name="action"></slot>
|
||||
</slot>
|
||||
</view>
|
||||
<view class="card-content" :class="{ 'no-padding': noPadding }">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<view v-if="$slots.footer" class="card-footer">
|
||||
<slot name="footer"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Card',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
noPadding: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
clickable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
if (this.clickable) {
|
||||
this.$emit('click');
|
||||
uni.vibrateShort();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
background: $uni-bg-color;
|
||||
border-radius: $uni-border-radius-lg;
|
||||
box-shadow: $uni-shadow;
|
||||
transition: $uni-transition;
|
||||
overflow: hidden;
|
||||
margin: $uni-spacing-col-lg;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $uni-shadow-hover;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: $uni-spacing-col-lg $uni-spacing-row-xl;
|
||||
border-bottom: 1px solid $uni-border-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: $uni-font-size-lg;
|
||||
font-weight: 600;
|
||||
color: $uni-text-color;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: $uni-spacing-col-xl;
|
||||
|
||||
&.no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: $uni-spacing-col-lg $uni-spacing-row-xl;
|
||||
border-top: 1px solid $uni-border-color;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.card {
|
||||
margin: $uni-spacing-col-base;
|
||||
}
|
||||
|
||||
.card-header,
|
||||
.card-content,
|
||||
.card-footer {
|
||||
padding-left: $uni-spacing-row-lg;
|
||||
padding-right: $uni-spacing-row-lg;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<view class="grid-layout" :class="gridClass">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'GridLayout',
|
||||
props: {
|
||||
columns: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
validator: (value) => [2, 3, 4].includes(value),
|
||||
},
|
||||
gap: {
|
||||
type: String,
|
||||
default: '12px',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
gridClass() {
|
||||
return `grid-${this.columns}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.grid-layout {
|
||||
display: grid;
|
||||
gap: v-bind(gap);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.grid-3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.grid-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 375px) {
|
||||
.grid-4 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.grid-3 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 320px) {
|
||||
.grid-4,
|
||||
.grid-3 {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<view class="navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="navbar-content">
|
||||
<text class="navbar-title">{{ title }}</text>
|
||||
<view class="navbar-actions">
|
||||
<slot name="actions">
|
||||
<view
|
||||
v-if="showRefresh"
|
||||
class="btn-icon refresh-btn touchable"
|
||||
@click="handleRefresh"
|
||||
:aria-label="refreshLabel"
|
||||
>
|
||||
<text class="icon">🔄</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="showSearch"
|
||||
class="btn-icon touchable"
|
||||
@click="handleSearch"
|
||||
:aria-label="searchLabel"
|
||||
>
|
||||
<text class="icon">🔍</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="showSettings"
|
||||
class="btn-icon touchable"
|
||||
@click="handleSettings"
|
||||
:aria-label="settingsLabel"
|
||||
>
|
||||
<text class="icon">⚙️</text>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NavBar',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '智慧校园',
|
||||
},
|
||||
showRefresh: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showSettings: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
refreshLabel: {
|
||||
type: String,
|
||||
default: '刷新',
|
||||
},
|
||||
searchLabel: {
|
||||
type: String,
|
||||
default: '搜索',
|
||||
},
|
||||
settingsLabel: {
|
||||
type: String,
|
||||
default: '设置',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
statusBarHeight: 20,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// 获取状态栏高度
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
this.statusBarHeight = systemInfo.statusBarHeight || 20;
|
||||
},
|
||||
methods: {
|
||||
handleRefresh() {
|
||||
this.$emit('refresh');
|
||||
// 触觉反馈
|
||||
uni.vibrateShort();
|
||||
},
|
||||
handleSearch() {
|
||||
this.$emit('search');
|
||||
uni.vibrateShort();
|
||||
},
|
||||
handleSettings() {
|
||||
this.$emit('settings');
|
||||
uni.vibrateShort();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
background: linear-gradient(135deg, $uni-color-primary, $uni-color-primary-dark);
|
||||
color: $uni-text-color-inverse;
|
||||
padding-bottom: 16px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
box-shadow: $uni-shadow;
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 $uni-spacing-row-lg;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: $uni-font-size-xl;
|
||||
font-weight: 600;
|
||||
color: $uni-text-color-inverse;
|
||||
}
|
||||
|
||||
.navbar-actions {
|
||||
display: flex;
|
||||
gap: $uni-spacing-row-base;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: $uni-border-radius-circle;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: $uni-transition;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 18px;
|
||||
color: $uni-text-color-inverse;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.refresh-btn {
|
||||
animation: none;
|
||||
|
||||
&:active {
|
||||
animation: spin 0.6s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 无障碍支持 */
|
||||
.btn-icon[aria-label] {
|
||||
position: relative;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.5);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<view
|
||||
class="service-item touchable"
|
||||
@click="handleClick"
|
||||
:aria-label="`${title}服务`"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<view class="service-icon">
|
||||
<text class="icon" v-if="icon">{{ icon }}</text>
|
||||
<slot v-else name="icon"></slot>
|
||||
</view>
|
||||
<text class="service-title">{{ title }}</text>
|
||||
<view v-if="badge" class="service-badge">
|
||||
<text class="badge-text">{{ badge }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ServiceItem',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
badge: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.$emit('click', { title: this.title });
|
||||
uni.vibrateShort();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.service-item {
|
||||
background: $uni-bg-color;
|
||||
border-radius: $uni-border-radius-lg;
|
||||
padding: $uni-spacing-col-xl;
|
||||
text-align: center;
|
||||
box-shadow: $uni-shadow;
|
||||
transition: $uni-transition;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: $uni-shadow-hover;
|
||||
border-color: $uni-color-primary;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $uni-color-primary;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: $uni-color-primary-light;
|
||||
border-radius: $uni-border-radius-circle;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto $uni-spacing-col-base;
|
||||
color: $uni-color-primary;
|
||||
transition: $uni-transition;
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.service-item:hover .service-icon {
|
||||
background: $uni-color-primary;
|
||||
color: $uni-text-color-inverse;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.service-title {
|
||||
font-size: $uni-font-size-base;
|
||||
font-weight: 500;
|
||||
color: $uni-text-color;
|
||||
display: block;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.service-badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
background: #ff4757;
|
||||
border-radius: $uni-border-radius-lg;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.badge-text {
|
||||
color: $uni-text-color-inverse;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 网格布局支持 */
|
||||
.grid-2 .service-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.grid-3 .service-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.grid-4 .service-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.service-item {
|
||||
animation: fadeInUp 0.6s ease-out backwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.service-item {
|
||||
padding: $uni-spacing-col-lg;
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
.icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.service-title {
|
||||
font-size: $uni-font-size-sm;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<view class="stat-card touchable" @click="handleClick">
|
||||
<text class="stat-number">{{ number }}</text>
|
||||
<text class="stat-label">{{ label }}</text>
|
||||
<view v-if="trend" class="stat-trend" :class="trendClass">
|
||||
<text class="trend-icon">{{ trendIcon }}</text>
|
||||
<text class="trend-text">{{ trend }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'StatCard',
|
||||
props: {
|
||||
number: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
trend: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
trendType: {
|
||||
type: String,
|
||||
default: 'neutral', // 'up', 'down', 'neutral'
|
||||
validator: (value) => ['up', 'down', 'neutral'].includes(value),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
trendClass() {
|
||||
return `trend-${this.trendType}`;
|
||||
},
|
||||
trendIcon() {
|
||||
switch (this.trendType) {
|
||||
case 'up':
|
||||
return '↗️';
|
||||
case 'down':
|
||||
return '↘️';
|
||||
default:
|
||||
return '➖';
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.$emit('click', { number: this.number, label: this.label });
|
||||
uni.vibrateShort();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.stat-card {
|
||||
background: $uni-bg-color;
|
||||
border-radius: $uni-border-radius-lg;
|
||||
padding: $uni-spacing-col-xl;
|
||||
text-align: center;
|
||||
box-shadow: $uni-shadow;
|
||||
transition: $uni-transition;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: $uni-shadow-hover;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: $uni-color-primary;
|
||||
margin-bottom: $uni-spacing-col-sm;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: $uni-font-size-sm;
|
||||
color: $uni-text-color-secondary;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: $uni-spacing-col-sm;
|
||||
font-size: $uni-font-size-sm;
|
||||
|
||||
&.trend-up {
|
||||
color: $uni-color-success;
|
||||
}
|
||||
|
||||
&.trend-down {
|
||||
color: $uni-color-error;
|
||||
}
|
||||
|
||||
&.trend-neutral {
|
||||
color: $uni-text-color-grey;
|
||||
}
|
||||
}
|
||||
|
||||
.trend-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.trend-text {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.stat-card {
|
||||
animation: bounceIn 0.6s ease-out backwards;
|
||||
}
|
||||
|
||||
@keyframes bounceIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.3);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
70% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 数字计数动画 */
|
||||
.stat-number {
|
||||
animation: countUp 1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes countUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.stat-card {
|
||||
padding: $uni-spacing-col-lg;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* 应用配置文件
|
||||
*/
|
||||
|
||||
// 环境配置
|
||||
const ENV_CONFIG = {
|
||||
// 开发环境
|
||||
development: {
|
||||
BASE_URL: 'http://localhost:8080',
|
||||
TIMEOUT: 10000,
|
||||
},
|
||||
// 生产环境
|
||||
production: {
|
||||
BASE_URL: 'https://your-api-domain.com',
|
||||
TIMEOUT: 15000,
|
||||
},
|
||||
// 测试环境
|
||||
test: {
|
||||
BASE_URL: 'http://test-api.your-domain.com',
|
||||
TIMEOUT: 10000,
|
||||
},
|
||||
};
|
||||
|
||||
// 当前环境(可以通过构建工具设置,这里默认为开发环境)
|
||||
const CURRENT_ENV = process.env.NODE_ENV || 'development';
|
||||
|
||||
// 导出当前环境配置
|
||||
export const config = ENV_CONFIG[CURRENT_ENV];
|
||||
|
||||
// 导出所有配置供其他模块使用
|
||||
export default {
|
||||
...config,
|
||||
// 应用信息
|
||||
APP_NAME: '校园管理移动端',
|
||||
APP_VERSION: '1.0.0',
|
||||
// API相关配置
|
||||
API_PREFIX: '/api',
|
||||
// 存储配置
|
||||
STORAGE_PREFIX: 'xgxt_mobile_',
|
||||
// 其他配置
|
||||
PAGE_SIZE: 10,
|
||||
MAX_FILE_SIZE: 10 * 1024 * 1024, // 10MB
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
import { createSSRApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App);
|
||||
|
||||
return {
|
||||
app,
|
||||
};
|
||||
}
|
||||
import { createSSRApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import uviewPlus from 'uview-plus'
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
app.use(uviewPlus)
|
||||
return {
|
||||
app
|
||||
}
|
||||
}
|
||||
@@ -1,66 +1,72 @@
|
||||
{
|
||||
"name": "xgxt-mobile",
|
||||
"appid": "__UNI__XGXT_MOBILE",
|
||||
"description": "校园管理移动端",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": "100",
|
||||
"transformPx": false,
|
||||
"app-plus": {
|
||||
"usingComponents": true,
|
||||
"nvueStyleCompiler": "uni-app",
|
||||
"compilerVersion": 3,
|
||||
"splashscreen": {
|
||||
"alwaysShowBeforeRender": true,
|
||||
"waiting": true,
|
||||
"autoclose": true,
|
||||
"delay": 0
|
||||
"name" : "",
|
||||
"appid" : "",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
"nvueStyleCompiler" : "uni-app",
|
||||
"compilerVersion" : 3,
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
},
|
||||
"modules": {},
|
||||
"distribute": {
|
||||
"android": {
|
||||
"permissions": [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\" />",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\" />",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\" />",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\" />",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\" />",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\" />",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\" />",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\" />",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\" />",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\" />",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\" />",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\" />"
|
||||
/* 模块配置 */
|
||||
"modules" : {},
|
||||
/* 应用发布信息 */
|
||||
"distribute" : {
|
||||
/* android打包配置 */
|
||||
"android" : {
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
"ios": {},
|
||||
"sdkConfigs": {}
|
||||
/* ios打包配置 */
|
||||
"ios" : {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
},
|
||||
"quickapp": {},
|
||||
"mp-weixin": {
|
||||
"appid": "",
|
||||
"setting": {
|
||||
"urlCheck": false
|
||||
/* 快应用特有相关 */
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
"usingComponents": true
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-alipay": {
|
||||
"usingComponents": true
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-baidu": {
|
||||
"usingComponents": true
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-toutiao": {
|
||||
"usingComponents": true
|
||||
"mp-toutiao" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"uniStatistics": {
|
||||
"uniStatistics": {
|
||||
"enable": false
|
||||
},
|
||||
"h5": {
|
||||
"title": "校园管理移动端",
|
||||
"template": "index.html"
|
||||
}
|
||||
}
|
||||
"vueVersion" : "3"
|
||||
}
|
||||
@@ -1,80 +1,61 @@
|
||||
{
|
||||
"easycom": {
|
||||
"^u-(.*)": "@/uni_modules/uview-ui/components/u-$1/u-$1.vue"
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/login/login",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "首页",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/workbench/workbench",
|
||||
"style": {
|
||||
"navigationBarTitleText": "工作台",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/mine",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "校园管理",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F8F8F8",
|
||||
"app-plus": {
|
||||
"background": "#efeff4"
|
||||
}
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#7A7E83",
|
||||
"selectedColor": "#3c9cff",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderStyle": "black",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"iconPath": "static/tabbar/home.png",
|
||||
"selectedIconPath": "static/tabbar/home-active.png",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/workbench/workbench",
|
||||
"iconPath": "static/tabbar/workbench.png",
|
||||
"selectedIconPath": "static/tabbar/workbench-active.png",
|
||||
"text": "工作台"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/mine/mine",
|
||||
"iconPath": "static/tabbar/mine.png",
|
||||
"selectedIconPath": "static/tabbar/mine-active.png",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
"condition": {
|
||||
"current": 0,
|
||||
"list": [
|
||||
{
|
||||
"name": "首页",
|
||||
"path": "pages/index/index",
|
||||
"query": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "首页",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/workbench/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "办公中心",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "智慧校园",
|
||||
"navigationBarBackgroundColor": "rgb(93, 135, 255)",
|
||||
"backgroundColor": "#f8f9fa"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#999999",
|
||||
"selectedColor": "rgb(93, 135, 255)",
|
||||
"borderStyle": "white",
|
||||
"backgroundColor": "#ffffff",
|
||||
"height": "60px",
|
||||
"fontSize": "12px",
|
||||
"iconWidth": "22px",
|
||||
"iconHeight": "22px",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页",
|
||||
"iconPath": "static/tabbar/home.svg",
|
||||
"selectedIconPath": "static/tabbar/home-active.svg"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/workbench/index",
|
||||
"text": "办公中心",
|
||||
"iconPath": "static/tabbar/workbench.svg",
|
||||
"selectedIconPath": "static/tabbar/workbench-active.svg"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/index",
|
||||
"text": "我的",
|
||||
"iconPath": "static/tabbar/profile.svg",
|
||||
"selectedIconPath": "static/tabbar/profile-active.svg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,353 +1,631 @@
|
||||
<template>
|
||||
<view class="index-container">
|
||||
<!-- 自定义导航栏 -->
|
||||
<u-navbar
|
||||
:is-back="false"
|
||||
:border-bottom="false"
|
||||
:custom-style="{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }"
|
||||
>
|
||||
<template #left>
|
||||
<view class="nav-left">
|
||||
<u-avatar :src="userInfo.avatar" size="60"></u-avatar>
|
||||
<view class="user-info">
|
||||
<text class="nickname">{{ userInfo.nickname }}</text>
|
||||
<text class="time">{{ currentTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template #right>
|
||||
<u-icon name="bell" color="#fff" size="44" @click="showNotifications"></u-icon>
|
||||
</template>
|
||||
</u-navbar>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<view class="content">
|
||||
<!-- 轮播图 -->
|
||||
<view class="banner-section">
|
||||
<u-swiper
|
||||
:list="bannerList"
|
||||
:autoplay="true"
|
||||
:circular="true"
|
||||
height="300"
|
||||
border-radius="20"
|
||||
@click="onBannerClick"
|
||||
></u-swiper>
|
||||
</view>
|
||||
|
||||
<!-- 快捷功能 -->
|
||||
<view class="quick-actions card">
|
||||
<view class="section-title">
|
||||
<text>快捷功能</text>
|
||||
</view>
|
||||
<view class="actions-grid">
|
||||
<view
|
||||
v-for="(item, index) in quickActions"
|
||||
:key="index"
|
||||
class="action-item"
|
||||
@click="onActionClick(item)"
|
||||
>
|
||||
<view class="action-icon" :style="{ background: item.color }">
|
||||
<u-icon :name="item.icon" color="#fff" size="40"></u-icon>
|
||||
</view>
|
||||
<text class="action-text">{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据统计 -->
|
||||
<view class="statistics card">
|
||||
<view class="section-title">
|
||||
<text>数据统计</text>
|
||||
</view>
|
||||
<view class="stats-grid">
|
||||
<view v-for="(item, index) in statistics" :key="index" class="stat-item">
|
||||
<text class="stat-value">{{ item.value }}</text>
|
||||
<text class="stat-label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最新动态 -->
|
||||
<view class="news-section card">
|
||||
<view class="section-title">
|
||||
<text>最新动态</text>
|
||||
<text class="more" @click="goToNews">查看更多</text>
|
||||
</view>
|
||||
<view class="news-list">
|
||||
<view
|
||||
v-for="(item, index) in newsList"
|
||||
:key="index"
|
||||
class="news-item"
|
||||
@click="onNewsClick(item)"
|
||||
>
|
||||
<view class="news-content">
|
||||
<text class="news-title">{{ item.title }}</text>
|
||||
<text class="news-time">{{ item.time }}</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#c8c9cc" size="28"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Index',
|
||||
data() {
|
||||
return {
|
||||
userInfo: {},
|
||||
currentTime: '',
|
||||
bannerList: [
|
||||
{ image: '/static/logo.png', title: '欢迎使用校园管理系统' },
|
||||
{ image: '/static/logo.png', title: '便捷高效的移动办公' },
|
||||
{ image: '/static/logo.png', title: '智慧校园,美好未来' },
|
||||
],
|
||||
quickActions: [
|
||||
{ name: '考勤管理', icon: 'clock', color: '#ff6b6b' },
|
||||
{ name: '请假申请', icon: 'file-text', color: '#4ecdc4' },
|
||||
{ name: '工资查询', icon: 'rmb-circle', color: '#45b7d1' },
|
||||
{ name: '通知公告', icon: 'volume', color: '#f9ca24' },
|
||||
{ name: '课程表', icon: 'table', color: '#6c5ce7' },
|
||||
{ name: '成绩查询', icon: 'star', color: '#a29bfe' },
|
||||
{ name: '图书馆', icon: 'book', color: '#fd79a8' },
|
||||
{ name: '更多', icon: 'grid', color: '#636e72' },
|
||||
],
|
||||
statistics: [
|
||||
{ label: '今日签到', value: '156' },
|
||||
{ label: '本月请假', value: '23' },
|
||||
{ label: '待审批', value: '8' },
|
||||
{ label: '已完成', value: '234' },
|
||||
],
|
||||
newsList: [
|
||||
{ title: '关于2024年春季学期开学安排的通知', time: '2024-02-15 09:30' },
|
||||
{ title: '校园疫情防控最新要求', time: '2024-02-14 16:20' },
|
||||
{ title: '学生宿舍管理规定更新', time: '2024-02-13 14:10' },
|
||||
{ title: '图书馆开放时间调整通知', time: '2024-02-12 11:40' },
|
||||
],
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.initPage();
|
||||
},
|
||||
onShow() {
|
||||
this.updateTime();
|
||||
},
|
||||
methods: {
|
||||
// 初始化页面
|
||||
initPage() {
|
||||
this.getUserInfo();
|
||||
this.updateTime();
|
||||
this.setTimeInterval();
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo() {
|
||||
const userInfo = uni.getStorageSync('userInfo');
|
||||
if (userInfo) {
|
||||
this.userInfo = userInfo;
|
||||
} else {
|
||||
// 如果没有用户信息,跳转到登录页
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/login',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 更新时间
|
||||
updateTime() {
|
||||
const now = new Date();
|
||||
const hour = now.getHours();
|
||||
let greeting = '早上好';
|
||||
if (hour >= 12 && hour < 18) {
|
||||
greeting = '下午好';
|
||||
} else if (hour >= 18) {
|
||||
greeting = '晚上好';
|
||||
}
|
||||
this.currentTime = greeting;
|
||||
},
|
||||
|
||||
// 设置时间更新定时器
|
||||
setTimeInterval() {
|
||||
setInterval(() => {
|
||||
this.updateTime();
|
||||
}, 60000); // 每分钟更新一次
|
||||
},
|
||||
|
||||
// 轮播图点击
|
||||
onBannerClick(index) {
|
||||
console.log('点击轮播图', index);
|
||||
},
|
||||
|
||||
// 快捷功能点击
|
||||
onActionClick(item) {
|
||||
console.log('点击功能', item.name);
|
||||
this.$u.toast(item.name);
|
||||
},
|
||||
|
||||
// 新闻点击
|
||||
onNewsClick(item) {
|
||||
console.log('点击新闻', item.title);
|
||||
this.$u.toast('查看:' + item.title);
|
||||
},
|
||||
|
||||
// 显示通知
|
||||
showNotifications() {
|
||||
this.$u.toast('暂无新通知');
|
||||
},
|
||||
|
||||
// 查看更多新闻
|
||||
goToNews() {
|
||||
this.$u.toast('跳转到新闻列表');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.index-container {
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.nav-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
|
||||
.nickname {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.banner-section {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
text {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.more {
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
.actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 30rpx;
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
|
||||
.action-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
color: #303133;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.statistics {
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20rpx;
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 30rpx 0;
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #3c9cff;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.news-section {
|
||||
.news-list {
|
||||
.news-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx 0;
|
||||
border-bottom: 2rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.news-content {
|
||||
flex: 1;
|
||||
|
||||
.news-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #303133;
|
||||
margin-bottom: 10rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.news-time {
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<!-- 自定义导航栏 -->
|
||||
<NavBar
|
||||
title="智慧校园"
|
||||
:show-refresh="true"
|
||||
:show-search="true"
|
||||
@refresh="handleRefresh"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<scroll-view class="main-content" scroll-y enhanced :show-scrollbar="false">
|
||||
<!-- 欢迎横幅 -->
|
||||
<view class="welcome-banner">
|
||||
<view class="welcome-content">
|
||||
<view class="user-info">
|
||||
<text class="greeting">你好,{{ userInfo.name }}</text>
|
||||
<text class="date-time">{{ currentDateTime }}</text>
|
||||
</view>
|
||||
<view class="weather-info">
|
||||
<text class="weather-icon">{{ weather.icon }}</text>
|
||||
<text class="weather-text">
|
||||
{{ weather.temperature }} {{ weather.condition }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快速统计 -->
|
||||
<view class="stats-section">
|
||||
<GridLayout :columns="2" gap="12px">
|
||||
<StatCard
|
||||
v-for="stat in stats"
|
||||
:key="stat.label"
|
||||
:number="stat.number"
|
||||
:label="stat.label"
|
||||
:trend="stat.trend"
|
||||
:trend-type="stat.trendType"
|
||||
@click="handleStatClick"
|
||||
/>
|
||||
</GridLayout>
|
||||
</view>
|
||||
|
||||
<!-- 常用服务 -->
|
||||
<Card title="常用服务">
|
||||
<template #action>
|
||||
<text class="more-link" @click="navigateToService">查看更多 ›</text>
|
||||
</template>
|
||||
<GridLayout :columns="4" gap="12px">
|
||||
<ServiceItem
|
||||
v-for="service in commonServices"
|
||||
:key="service.title"
|
||||
:title="service.title"
|
||||
:icon="service.icon"
|
||||
:badge="service.badge"
|
||||
@click="handleServiceClick"
|
||||
/>
|
||||
</GridLayout>
|
||||
</Card>
|
||||
|
||||
<!-- 待办任务 -->
|
||||
<Card>
|
||||
<template #header>
|
||||
<view class="todo-header">
|
||||
<view class="todo-title-wrapper">
|
||||
<text class="todo-icon">✓</text>
|
||||
<text class="card-title">待办任务</text>
|
||||
</view>
|
||||
<view class="todo-badge-wrapper">
|
||||
<text class="todo-badge">{{ todoList.length }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template #default>
|
||||
<view class="todo-list">
|
||||
<view
|
||||
v-for="todo in todoList.slice(0, 3)"
|
||||
:key="todo.id"
|
||||
class="todo-item touchable"
|
||||
@click="handleTodoClick(todo)"
|
||||
>
|
||||
<view class="todo-icon-wrapper">
|
||||
<text class="todo-item-icon">{{ todo.icon }}</text>
|
||||
</view>
|
||||
<view class="todo-content">
|
||||
<text class="todo-title">{{ todo.title }}</text>
|
||||
<text class="todo-desc">{{ todo.description }}</text>
|
||||
</view>
|
||||
<view v-if="todo.urgent" class="urgent-badge">
|
||||
<text class="urgent-text">紧急</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="todoList.length > 3"
|
||||
class="view-more-todos"
|
||||
@click="viewAllTodos"
|
||||
>
|
||||
<text class="view-more-text">
|
||||
查看全部 {{ todoList.length }} 项任务
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<!-- 校园动态 -->
|
||||
<Card>
|
||||
<template #header>
|
||||
<view class="news-header">
|
||||
<view class="news-title-wrapper">
|
||||
<text class="news-icon">📢</text>
|
||||
<text class="card-title">校园动态</text>
|
||||
</view>
|
||||
<text class="more-link" @click="viewAllNews">查看全部 ›</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #default>
|
||||
<view class="news-list">
|
||||
<view
|
||||
v-for="news in newsList.slice(0, 4)"
|
||||
:key="news.id"
|
||||
class="news-item touchable"
|
||||
@click="handleNewsClick(news)"
|
||||
>
|
||||
<view class="news-header-info">
|
||||
<text class="news-title">{{ news.title }}</text>
|
||||
<text class="news-time">{{ news.time }}</text>
|
||||
</view>
|
||||
<text class="news-content">{{ news.content }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<!-- 快捷功能 -->
|
||||
<Card>
|
||||
<template #header>
|
||||
<view class="quick-header">
|
||||
<text class="quick-icon">⚡</text>
|
||||
<text class="card-title">快捷功能</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #default>
|
||||
<GridLayout :columns="3" gap="12px">
|
||||
<ServiceItem
|
||||
v-for="tool in quickTools"
|
||||
:key="tool.title"
|
||||
:title="tool.title"
|
||||
:icon="tool.icon"
|
||||
@click="handleToolClick"
|
||||
/>
|
||||
</GridLayout>
|
||||
</template>
|
||||
</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 StatCard from '@/components/StatCard.vue';
|
||||
import GridLayout from '@/components/GridLayout.vue';
|
||||
|
||||
export default {
|
||||
name: 'HomePage',
|
||||
components: {
|
||||
NavBar,
|
||||
Card,
|
||||
ServiceItem,
|
||||
StatCard,
|
||||
GridLayout,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
userInfo: {
|
||||
name: '张同学',
|
||||
},
|
||||
currentDateTime: '',
|
||||
weather: {
|
||||
icon: '☁️',
|
||||
temperature: '22°C',
|
||||
condition: '多云',
|
||||
},
|
||||
stats: [
|
||||
{ number: 5, label: '待办任务', trend: '+2', trendType: 'up' },
|
||||
{ number: 8, label: '新消息', trend: '+3', trendType: 'up' },
|
||||
{ number: 95, label: '学分', trend: '+5', trendType: 'up' },
|
||||
{ number: 4.2, label: 'GPA', trend: '+0.1', trendType: 'up' },
|
||||
],
|
||||
commonServices: [
|
||||
{ title: '课程表', icon: '🎓' },
|
||||
{ title: '成绩查询', icon: '📊' },
|
||||
{ title: '图书馆', icon: '📚' },
|
||||
{ title: '食堂', icon: '🍽️' },
|
||||
{ title: '校园卡', icon: '💳' },
|
||||
{ title: '考试安排', icon: '📝' },
|
||||
{ title: '选课', icon: '👨🎓' },
|
||||
{ title: '更多', icon: '➕' },
|
||||
],
|
||||
todoList: [
|
||||
{
|
||||
id: 1,
|
||||
title: '期末作业提交',
|
||||
description: '《数据结构》课程作业,截止时间:12月20日',
|
||||
icon: '📄',
|
||||
urgent: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '图书续借',
|
||||
description: '《算法导论》即将到期,需要续借',
|
||||
icon: '📖',
|
||||
urgent: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '学费缴费',
|
||||
description: '下学期学费缴费通知已发布',
|
||||
icon: '💰',
|
||||
urgent: false,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '课程评教',
|
||||
description: '本学期课程评教开始,请及时完成',
|
||||
icon: '📋',
|
||||
urgent: false,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '身份认证',
|
||||
description: '学生身份信息需要更新完善',
|
||||
icon: '👤',
|
||||
urgent: false,
|
||||
},
|
||||
],
|
||||
newsList: [
|
||||
{
|
||||
id: 1,
|
||||
title: '关于2024年春季学期选课通知',
|
||||
content:
|
||||
'2024年春季学期选课工作即将开始,请同学们关注选课时间安排,及时完成选课...',
|
||||
time: '刚刚',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '图书馆元旦假期开放时间调整',
|
||||
content: '为保障同学们假期学习需求,图书馆元旦假期开放时间调整为9:00-17:00...',
|
||||
time: '2小时前',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '2024年国家奖学金评选结果公示',
|
||||
content: '经过严格评选,2024年国家奖学金获得者名单现予以公示,公示期为一周...',
|
||||
time: '1天前',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '校园网络维护通知',
|
||||
content:
|
||||
'为提升网络服务质量,信息中心将于本周末进行网络设备维护,期间可能出现短暂断网...',
|
||||
time: '2天前',
|
||||
},
|
||||
],
|
||||
quickTools: [
|
||||
{ title: '扫一扫', icon: '📱' },
|
||||
{ title: '通讯录', icon: '📞' },
|
||||
{ title: '校园地图', icon: '🗺️' },
|
||||
{ title: '校车时刻', icon: '🚌' },
|
||||
{ title: '校历', icon: '📅' },
|
||||
{ title: '帮助', icon: '❓' },
|
||||
],
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.updateDateTime();
|
||||
this.startTimeUpdate();
|
||||
},
|
||||
onUnload() {
|
||||
if (this.timeTimer) {
|
||||
clearInterval(this.timeTimer);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateDateTime() {
|
||||
const now = new Date();
|
||||
const month = now.getMonth() + 1;
|
||||
const day = now.getDate();
|
||||
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
|
||||
const weekday = weekdays[now.getDay()];
|
||||
this.currentDateTime = `${month}月${day}日 星期${weekday}`;
|
||||
},
|
||||
startTimeUpdate() {
|
||||
this.timeTimer = setInterval(() => {
|
||||
this.updateDateTime();
|
||||
}, 60000); // 每分钟更新一次
|
||||
},
|
||||
handleRefresh() {
|
||||
uni.showLoading({ title: '刷新中...' });
|
||||
setTimeout(() => {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '刷新成功', icon: 'success' });
|
||||
}, 1000);
|
||||
},
|
||||
handleSearch() {
|
||||
uni.showToast({ title: '搜索功能开发中', icon: 'none' });
|
||||
},
|
||||
handleStatClick(stat) {
|
||||
uni.showToast({ title: `查看${stat.label}详情`, icon: 'none' });
|
||||
},
|
||||
handleServiceClick(service) {
|
||||
uni.showToast({ title: `打开${service.title}`, icon: 'none' });
|
||||
},
|
||||
handleTodoClick(todo) {
|
||||
uni.showToast({ title: `处理:${todo.title}`, icon: 'none' });
|
||||
},
|
||||
handleNewsClick(news) {
|
||||
uni.showToast({ title: `查看:${news.title}`, icon: 'none' });
|
||||
},
|
||||
handleToolClick(tool) {
|
||||
uni.showToast({ title: `使用${tool.title}`, icon: 'none' });
|
||||
},
|
||||
navigateToService() {
|
||||
uni.switchTab({ url: '/pages/workbench/index' });
|
||||
},
|
||||
viewAllTodos() {
|
||||
uni.showToast({ title: '查看全部待办', icon: 'none' });
|
||||
},
|
||||
viewAllNews() {
|
||||
uni.showToast({ title: '查看全部动态', icon: 'none' });
|
||||
},
|
||||
},
|
||||
};
|
||||
</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));
|
||||
}
|
||||
|
||||
/* 欢迎横幅 */
|
||||
.welcome-banner {
|
||||
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;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
font-size: $uni-font-size-xxl;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.date-time {
|
||||
font-size: $uni-font-size-base;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.weather-info {
|
||||
text-align: right;
|
||||
font-size: $uni-font-size-base;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.weather-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.weather-text {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 统计部分 */
|
||||
.stats-section {
|
||||
padding: 0 $uni-spacing-row-lg;
|
||||
margin-bottom: $uni-spacing-col-lg;
|
||||
}
|
||||
|
||||
/* 更多链接 */
|
||||
.more-link {
|
||||
font-size: $uni-font-size-base;
|
||||
color: $uni-color-primary;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* 待办任务 */
|
||||
.todo-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.todo-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.todo-icon {
|
||||
color: $uni-color-primary;
|
||||
font-size: $uni-font-size-lg;
|
||||
}
|
||||
|
||||
.todo-badge-wrapper {
|
||||
background: #ff4757;
|
||||
border-radius: $uni-border-radius-lg;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.todo-badge {
|
||||
color: $uni-text-color-inverse;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $uni-spacing-col-lg 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: $uni-transition;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $uni-color-primary-light;
|
||||
}
|
||||
}
|
||||
|
||||
.todo-icon-wrapper {
|
||||
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;
|
||||
}
|
||||
|
||||
.todo-item-icon {
|
||||
font-size: 18px;
|
||||
color: $uni-color-primary;
|
||||
}
|
||||
|
||||
.todo-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.todo-title {
|
||||
font-size: $uni-font-size-lg;
|
||||
font-weight: 500;
|
||||
color: $uni-text-color;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.todo-desc {
|
||||
font-size: $uni-font-size-sm;
|
||||
color: $uni-text-color-secondary;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.urgent-badge {
|
||||
background: #ff4757;
|
||||
color: $uni-text-color-inverse;
|
||||
border-radius: $uni-border-radius-lg;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.urgent-text {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.view-more-todos {
|
||||
padding: $uni-spacing-col-lg 0;
|
||||
text-align: center;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
margin-top: $uni-spacing-col-base;
|
||||
}
|
||||
|
||||
.view-more-text {
|
||||
color: $uni-color-primary;
|
||||
font-size: $uni-font-size-base;
|
||||
}
|
||||
|
||||
/* 校园动态 */
|
||||
.news-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.news-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.news-icon {
|
||||
font-size: $uni-font-size-lg;
|
||||
}
|
||||
|
||||
.news-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.news-item {
|
||||
padding: $uni-spacing-col-lg 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: $uni-transition;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $uni-color-primary-light;
|
||||
}
|
||||
}
|
||||
|
||||
.news-header-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: $uni-font-size-lg;
|
||||
font-weight: 500;
|
||||
color: $uni-text-color;
|
||||
flex: 1;
|
||||
margin-right: $uni-spacing-row-base;
|
||||
}
|
||||
|
||||
.news-time {
|
||||
font-size: $uni-font-size-sm;
|
||||
color: $uni-text-color-grey;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.news-content {
|
||||
font-size: $uni-font-size-sm;
|
||||
color: $uni-text-color-secondary;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 快捷功能 */
|
||||
.quick-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quick-icon {
|
||||
color: #1abc9c;
|
||||
font-size: $uni-font-size-lg;
|
||||
}
|
||||
|
||||
/* 底部间距 */
|
||||
.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);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,208 +0,0 @@
|
||||
<template>
|
||||
<view class="login-container">
|
||||
<!-- 头部区域 -->
|
||||
<view class="header">
|
||||
<view class="logo">
|
||||
<image src="/static/logo.png" mode="aspectFit" class="logo-img"></image>
|
||||
</view>
|
||||
<view class="title">校园管理系统</view>
|
||||
<view class="subtitle">欢迎登录</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<view class="form-container">
|
||||
<view class="form-item">
|
||||
<input
|
||||
v-model="loginForm.username"
|
||||
placeholder="请输入用户名"
|
||||
class="input-field"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
class="input-field"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button class="login-btn" :class="{ loading: loading }" @click="handleLogin">
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Login',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
loginForm: {
|
||||
username: 'admin',
|
||||
password: 'admin',
|
||||
},
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
// 检查是否已登录
|
||||
const token = uni.getStorageSync('token');
|
||||
if (token) {
|
||||
this.redirectToHome();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 处理登录
|
||||
async handleLogin() {
|
||||
// 简单验证
|
||||
if (!this.loginForm.username || !this.loginForm.password) {
|
||||
uni.showToast({
|
||||
title: '请输入用户名和密码',
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
// 模拟登录API调用
|
||||
await this.mockLogin();
|
||||
|
||||
// 保存登录状态
|
||||
uni.setStorageSync('token', 'mock-token-123456');
|
||||
uni.setStorageSync('userInfo', {
|
||||
id: 1,
|
||||
username: this.loginForm.username,
|
||||
nickname: '管理员',
|
||||
avatar: '/static/avatar.png',
|
||||
});
|
||||
|
||||
uni.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success',
|
||||
});
|
||||
|
||||
// 延迟跳转,让用户看到成功提示
|
||||
setTimeout(() => {
|
||||
this.redirectToHome();
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: '登录失败:' + error.message,
|
||||
icon: 'none',
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 模拟登录API
|
||||
mockLogin() {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
if (
|
||||
this.loginForm.username === 'admin' &&
|
||||
this.loginForm.password === 'admin'
|
||||
) {
|
||||
resolve({ success: true });
|
||||
} else {
|
||||
reject(new Error('用户名或密码错误'));
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
},
|
||||
|
||||
// 跳转到首页
|
||||
redirectToHome() {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index',
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 100rpx 60rpx 60rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 100rpx;
|
||||
|
||||
.logo {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.logo-img {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 60rpx 40rpx;
|
||||
box-shadow: 0 20rpx 40rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
padding: 0 20rpx;
|
||||
border: 2rpx solid #e4e7ed;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
border-color: #3c9cff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: #3c9cff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 44rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #2b85e4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,441 +0,0 @@
|
||||
<template>
|
||||
<view class="mine-container">
|
||||
<!-- 用户信息区域 -->
|
||||
<view class="user-section">
|
||||
<view class="user-bg">
|
||||
<image src="/static/mine-bg.jpg" mode="aspectFill" class="bg-image"></image>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<view class="user-avatar">
|
||||
<u-avatar :src="userInfo.avatar" size="120"></u-avatar>
|
||||
<view class="avatar-badge">
|
||||
<u-icon name="checkmark" color="#fff" size="20"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="user-details">
|
||||
<text class="username">{{ userInfo.nickname }}</text>
|
||||
<text class="user-title">{{ userInfo.title || '管理员' }}</text>
|
||||
</view>
|
||||
<view class="user-stats">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ userStats.loginDays }}</text>
|
||||
<text class="stat-label">连续登录</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ userStats.completedTasks }}</text>
|
||||
<text class="stat-label">完成任务</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ userStats.points }}</text>
|
||||
<text class="stat-label">积分</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能菜单 -->
|
||||
<view class="menu-section">
|
||||
<!-- 个人中心 -->
|
||||
<view class="menu-group card">
|
||||
<view class="group-title">个人中心</view>
|
||||
<view class="menu-list">
|
||||
<view
|
||||
v-for="(item, index) in personalMenus"
|
||||
:key="index"
|
||||
class="menu-item"
|
||||
@click="onMenuClick(item)"
|
||||
>
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon" :style="{ background: item.color }">
|
||||
<u-icon :name="item.icon" color="#fff" size="32"></u-icon>
|
||||
</view>
|
||||
<text class="menu-text">{{ item.name }}</text>
|
||||
</view>
|
||||
<view class="menu-right">
|
||||
<text class="menu-badge" v-if="item.badge">{{ item.badge }}</text>
|
||||
<u-icon name="arrow-right" color="#c8c9cc" size="28"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 应用设置 -->
|
||||
<view class="menu-group card">
|
||||
<view class="group-title">应用设置</view>
|
||||
<view class="menu-list">
|
||||
<view
|
||||
v-for="(item, index) in settingMenus"
|
||||
:key="index"
|
||||
class="menu-item"
|
||||
@click="onMenuClick(item)"
|
||||
>
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon" :style="{ background: item.color }">
|
||||
<u-icon :name="item.icon" color="#fff" size="32"></u-icon>
|
||||
</view>
|
||||
<text class="menu-text">{{ item.name }}</text>
|
||||
</view>
|
||||
<view class="menu-right">
|
||||
<u-switch
|
||||
v-if="item.type === 'switch'"
|
||||
v-model="item.value"
|
||||
@change="onSwitchChange(item)"
|
||||
></u-switch>
|
||||
<text class="menu-value" v-else-if="item.value">{{ item.value }}</text>
|
||||
<u-icon name="arrow-right" color="#c8c9cc" size="28" v-else></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 其他功能 -->
|
||||
<view class="menu-group card">
|
||||
<view class="group-title">其他功能</view>
|
||||
<view class="menu-list">
|
||||
<view
|
||||
v-for="(item, index) in otherMenus"
|
||||
:key="index"
|
||||
class="menu-item"
|
||||
@click="onMenuClick(item)"
|
||||
>
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon" :style="{ background: item.color }">
|
||||
<u-icon :name="item.icon" color="#fff" size="32"></u-icon>
|
||||
</view>
|
||||
<text class="menu-text">{{ item.name }}</text>
|
||||
</view>
|
||||
<view class="menu-right">
|
||||
<u-icon name="arrow-right" color="#c8c9cc" size="28"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退出登录 -->
|
||||
<view class="logout-section">
|
||||
<u-button
|
||||
type="error"
|
||||
:plain="true"
|
||||
@click="showLogoutConfirm"
|
||||
:custom-style="{ width: '100%', height: '88rpx' }"
|
||||
>
|
||||
退出登录
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Mine',
|
||||
data() {
|
||||
return {
|
||||
userInfo: {},
|
||||
userStats: {
|
||||
loginDays: 15,
|
||||
completedTasks: 128,
|
||||
points: 2580,
|
||||
},
|
||||
personalMenus: [
|
||||
{ name: '个人资料', icon: 'account', color: '#3c9cff' },
|
||||
{ name: '我的消息', icon: 'chat', color: '#5ac725', badge: '5' },
|
||||
{ name: '我的收藏', icon: 'heart', color: '#f9ae3d' },
|
||||
{ name: '我的任务', icon: 'list', color: '#f56c6c' },
|
||||
],
|
||||
settingMenus: [
|
||||
{ name: '消息推送', icon: 'bell', color: '#ff6b6b', type: 'switch', value: true },
|
||||
{ name: '夜间模式', icon: 'moon', color: '#4ecdc4', type: 'switch', value: false },
|
||||
{ name: '语言设置', icon: 'globe', color: '#45b7d1', value: '简体中文' },
|
||||
{ name: '清除缓存', icon: 'trash', color: '#f9ca24' },
|
||||
],
|
||||
otherMenus: [
|
||||
{ name: '意见反馈', icon: 'edit-pen', color: '#6c5ce7' },
|
||||
{ name: '帮助中心', icon: 'question-circle', color: '#a29bfe' },
|
||||
{ name: '关于我们', icon: 'info-circle', color: '#fd79a8' },
|
||||
{ name: '检查更新', icon: 'download', color: '#636e72' },
|
||||
],
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.initPage();
|
||||
},
|
||||
methods: {
|
||||
// 初始化页面
|
||||
initPage() {
|
||||
this.getUserInfo();
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo() {
|
||||
const userInfo = uni.getStorageSync('userInfo');
|
||||
if (userInfo) {
|
||||
this.userInfo = userInfo;
|
||||
}
|
||||
},
|
||||
|
||||
// 菜单点击
|
||||
onMenuClick(item) {
|
||||
console.log('点击菜单', item.name);
|
||||
|
||||
if (item.name === '清除缓存') {
|
||||
this.clearCache();
|
||||
} else if (item.name === '检查更新') {
|
||||
this.checkUpdate();
|
||||
} else {
|
||||
this.$u.toast(item.name);
|
||||
}
|
||||
},
|
||||
|
||||
// 开关变化
|
||||
onSwitchChange(item) {
|
||||
console.log('开关变化', item.name, item.value);
|
||||
this.$u.toast(`${item.name}已${item.value ? '开启' : '关闭'}`);
|
||||
},
|
||||
|
||||
// 清除缓存
|
||||
clearCache() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要清除缓存吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 模拟清除缓存
|
||||
setTimeout(() => {
|
||||
this.$u.toast('缓存清除成功');
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// 检查更新
|
||||
checkUpdate() {
|
||||
uni.showLoading({
|
||||
title: '检查中...',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
uni.hideLoading();
|
||||
this.$u.toast('当前已是最新版本');
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
// 显示退出确认
|
||||
showLogoutConfirm() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.logout();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// 退出登录
|
||||
logout() {
|
||||
// 清除本地存储
|
||||
uni.removeStorageSync('token');
|
||||
uni.removeStorageSync('userInfo');
|
||||
|
||||
this.$u.toast('退出成功');
|
||||
|
||||
// 跳转到登录页
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/login',
|
||||
});
|
||||
}, 1000);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mine-container {
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.user-section {
|
||||
position: relative;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.user-bg {
|
||||
height: 400rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.bg-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(102, 126, 234, 0.8) 0%,
|
||||
rgba(118, 75, 162, 0.8) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60rpx 40rpx 40rpx;
|
||||
|
||||
.user-avatar {
|
||||
position: relative;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.avatar-badge {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: #5ac725;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.user-details {
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.username {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.user-title {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.user-stats {
|
||||
display: flex;
|
||||
gap: 60rpx;
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-section {
|
||||
padding: 0 20rpx 40rpx;
|
||||
}
|
||||
|
||||
.menu-group {
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.group-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx 20rpx;
|
||||
border-bottom: 2rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.menu-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
|
||||
.menu-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 28rpx;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
|
||||
.menu-badge {
|
||||
background: #f56c6c;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 20rpx;
|
||||
min-width: 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu-value {
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logout-section {
|
||||
margin-top: 40rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,620 @@
|
||||
<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';
|
||||
|
||||
export default {
|
||||
name: 'ProfilePage',
|
||||
components: {
|
||||
NavBar,
|
||||
Card,
|
||||
StatCard,
|
||||
GridLayout,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
userInfo: {
|
||||
name: '张同学',
|
||||
major: '计算机科学与技术',
|
||||
grade: '大三',
|
||||
studentId: '2021001234',
|
||||
gpa: 4.2,
|
||||
credits: 95,
|
||||
ranking: 15,
|
||||
},
|
||||
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: {
|
||||
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.showToast({ title: '正在退出...', icon: 'loading' });
|
||||
setTimeout(() => {
|
||||
uni.showToast({ title: '已退出登录', icon: 'success' });
|
||||
// 这里可以跳转到登录页面
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
onLoad() {},
|
||||
};
|
||||
</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>
|
||||
@@ -0,0 +1,474 @@
|
||||
<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';
|
||||
|
||||
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',
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
onLoad() {},
|
||||
};
|
||||
</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>
|
||||
@@ -1,494 +0,0 @@
|
||||
<template>
|
||||
<view class="workbench-container">
|
||||
<!-- 自定义导航栏 -->
|
||||
<u-navbar
|
||||
title="工作台"
|
||||
:is-back="false"
|
||||
:border-bottom="false"
|
||||
:custom-style="{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }"
|
||||
title-color="#fff"
|
||||
>
|
||||
<template #right>
|
||||
<u-icon name="search" color="#fff" size="44" @click="showSearch"></u-icon>
|
||||
</template>
|
||||
</u-navbar>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<view class="content">
|
||||
<!-- 工作统计 -->
|
||||
<view class="work-stats card">
|
||||
<view class="section-title">
|
||||
<text>工作统计</text>
|
||||
<text class="date">{{ today }}</text>
|
||||
</view>
|
||||
<view class="stats-row">
|
||||
<view
|
||||
v-for="(item, index) in workStats"
|
||||
:key="index"
|
||||
class="stat-item"
|
||||
@click="onStatClick(item)"
|
||||
>
|
||||
<view class="stat-icon" :style="{ background: item.color }">
|
||||
<u-icon :name="item.icon" color="#fff" size="36"></u-icon>
|
||||
</view>
|
||||
<view class="stat-info">
|
||||
<text class="stat-value">{{ item.value }}</text>
|
||||
<text class="stat-label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 待办事项 -->
|
||||
<view class="todo-section card">
|
||||
<view class="section-title">
|
||||
<text>待办事项</text>
|
||||
<view class="badge" v-if="todoList.length > 0">{{ todoList.length }}</view>
|
||||
</view>
|
||||
<view class="todo-list" v-if="todoList.length > 0">
|
||||
<view
|
||||
v-for="(item, index) in todoList"
|
||||
:key="index"
|
||||
class="todo-item"
|
||||
@click="onTodoClick(item)"
|
||||
>
|
||||
<view class="todo-left">
|
||||
<view class="priority-dot" :class="item.priority"></view>
|
||||
<view class="todo-content">
|
||||
<text class="todo-title">{{ item.title }}</text>
|
||||
<text class="todo-time">{{ item.deadline }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="todo-status" :class="item.status">
|
||||
{{ getStatusText(item.status) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-state">
|
||||
<u-icon name="checkbox-mark" color="#c8c9cc" size="80"></u-icon>
|
||||
<text>暂无待办事项</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 工作流程 -->
|
||||
<view class="workflow-section card">
|
||||
<view class="section-title">
|
||||
<text>工作流程</text>
|
||||
</view>
|
||||
<view class="workflow-grid">
|
||||
<view
|
||||
v-for="(item, index) in workflowItems"
|
||||
:key="index"
|
||||
class="workflow-item"
|
||||
@click="onWorkflowClick(item)"
|
||||
>
|
||||
<view class="workflow-icon" :style="{ background: item.color }">
|
||||
<u-icon :name="item.icon" color="#fff" size="40"></u-icon>
|
||||
</view>
|
||||
<text class="workflow-text">{{ item.name }}</text>
|
||||
<view class="workflow-badge" v-if="item.badge">{{ item.badge }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快速操作 -->
|
||||
<view class="quick-ops card">
|
||||
<view class="section-title">
|
||||
<text>快速操作</text>
|
||||
</view>
|
||||
<view class="ops-list">
|
||||
<view
|
||||
v-for="(item, index) in quickOps"
|
||||
:key="index"
|
||||
class="ops-item"
|
||||
@click="onQuickOpClick(item)"
|
||||
>
|
||||
<view class="ops-left">
|
||||
<u-icon :name="item.icon" :color="item.color" size="40"></u-icon>
|
||||
<view class="ops-info">
|
||||
<text class="ops-title">{{ item.title }}</text>
|
||||
<text class="ops-desc">{{ item.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#c8c9cc" size="28"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Workbench',
|
||||
data() {
|
||||
return {
|
||||
today: '',
|
||||
workStats: [
|
||||
{ label: '今日任务', value: '8', icon: 'list', color: '#3c9cff' },
|
||||
{ label: '已完成', value: '5', icon: 'checkmark', color: '#5ac725' },
|
||||
{ label: '进行中', value: '2', icon: 'clock', color: '#f9ae3d' },
|
||||
{ label: '待审批', value: '1', icon: 'eye', color: '#f56c6c' },
|
||||
],
|
||||
todoList: [
|
||||
{
|
||||
title: '审核学生请假申请',
|
||||
deadline: '今天 18:00',
|
||||
priority: 'high',
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
title: '提交月度工作报告',
|
||||
deadline: '明天 12:00',
|
||||
priority: 'medium',
|
||||
status: 'in-progress',
|
||||
},
|
||||
{
|
||||
title: '参加部门例会',
|
||||
deadline: '2024-02-16 14:00',
|
||||
priority: 'low',
|
||||
status: 'pending',
|
||||
},
|
||||
],
|
||||
workflowItems: [
|
||||
{ name: '请假审批', icon: 'file-text', color: '#ff6b6b', badge: '3' },
|
||||
{ name: '报销审批', icon: 'rmb-circle', color: '#4ecdc4', badge: '1' },
|
||||
{ name: '采购审批', icon: 'shopping-cart', color: '#45b7d1' },
|
||||
{ name: '合同审批', icon: 'file', color: '#f9ca24' },
|
||||
],
|
||||
quickOps: [
|
||||
{
|
||||
title: '创建任务',
|
||||
desc: '快速创建新的工作任务',
|
||||
icon: 'plus-circle',
|
||||
color: '#3c9cff',
|
||||
},
|
||||
{
|
||||
title: '工作日志',
|
||||
desc: '记录今日工作内容',
|
||||
icon: 'edit-pen',
|
||||
color: '#5ac725',
|
||||
},
|
||||
{
|
||||
title: '团队协作',
|
||||
desc: '查看团队成员工作状态',
|
||||
icon: 'account',
|
||||
color: '#f9ae3d',
|
||||
},
|
||||
{
|
||||
title: '数据统计',
|
||||
desc: '查看工作效率统计',
|
||||
icon: 'bar-chart',
|
||||
color: '#f56c6c',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.initPage();
|
||||
},
|
||||
methods: {
|
||||
// 初始化页面
|
||||
initPage() {
|
||||
this.setToday();
|
||||
},
|
||||
|
||||
// 设置当前日期
|
||||
setToday() {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
this.today = `${year}-${month}-${day}`;
|
||||
},
|
||||
|
||||
// 获取状态文本
|
||||
getStatusText(status) {
|
||||
const statusMap = {
|
||||
pending: '待处理',
|
||||
'in-progress': '进行中',
|
||||
completed: '已完成',
|
||||
};
|
||||
return statusMap[status] || '未知';
|
||||
},
|
||||
|
||||
// 统计项点击
|
||||
onStatClick(item) {
|
||||
console.log('点击统计', item.label);
|
||||
this.$u.toast(`查看${item.label}`);
|
||||
},
|
||||
|
||||
// 待办事项点击
|
||||
onTodoClick(item) {
|
||||
console.log('点击待办', item.title);
|
||||
this.$u.toast(`处理:${item.title}`);
|
||||
},
|
||||
|
||||
// 工作流程点击
|
||||
onWorkflowClick(item) {
|
||||
console.log('点击工作流', item.name);
|
||||
this.$u.toast(`打开${item.name}`);
|
||||
},
|
||||
|
||||
// 快速操作点击
|
||||
onQuickOpClick(item) {
|
||||
console.log('点击快速操作', item.title);
|
||||
this.$u.toast(item.title);
|
||||
},
|
||||
|
||||
// 显示搜索
|
||||
showSearch() {
|
||||
this.$u.toast('搜索功能');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.workbench-container {
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
text {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: #f56c6c;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
min-width: 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.work-stats {
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 20rpx;
|
||||
background: #fafafa;
|
||||
border-radius: 12rpx;
|
||||
|
||||
.stat-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 20rpx;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.todo-section {
|
||||
.todo-list {
|
||||
.todo-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx 0;
|
||||
border-bottom: 2rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.todo-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
flex: 1;
|
||||
|
||||
.priority-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
|
||||
&.high {
|
||||
background: #f56c6c;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
background: #f9ae3d;
|
||||
}
|
||||
|
||||
&.low {
|
||||
background: #5ac725;
|
||||
}
|
||||
}
|
||||
|
||||
.todo-content {
|
||||
.todo-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #303133;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.todo-time {
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.todo-status {
|
||||
font-size: 24rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
|
||||
&.pending {
|
||||
background: #fef0f0;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
&.in-progress {
|
||||
background: #fdf6ec;
|
||||
color: #f9ae3d;
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background: #f0f9ff;
|
||||
color: #5ac725;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 80rpx 0;
|
||||
color: #c8c9cc;
|
||||
|
||||
text {
|
||||
display: block;
|
||||
margin-top: 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.workflow-section {
|
||||
.workflow-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 30rpx;
|
||||
|
||||
.workflow-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
position: relative;
|
||||
|
||||
.workflow-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.workflow-text {
|
||||
font-size: 24rpx;
|
||||
color: #303133;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.workflow-badge {
|
||||
position: absolute;
|
||||
top: -10rpx;
|
||||
right: 10rpx;
|
||||
background: #f56c6c;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 10rpx;
|
||||
min-width: 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-ops {
|
||||
.ops-list {
|
||||
.ops-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx 0;
|
||||
border-bottom: 2rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ops-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
flex: 1;
|
||||
|
||||
.ops-info {
|
||||
.ops-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #303133;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.ops-desc {
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
export {};
|
||||
|
||||
declare module "vue" {
|
||||
type Hooks = App.AppInstance & Page.PageInstance;
|
||||
interface ComponentCustomOptions extends Hooks {}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
# 默认头像占位文件
|
||||
# 实际使用时请替换为对应的头像图片文件
|
||||
@@ -0,0 +1 @@
|
||||
/* 空的iconfont文件 - 用于解决依赖问题 */
|
||||
|
Before Width: | Height: | Size: 80 B After Width: | Height: | Size: 3.9 KiB |
@@ -1,2 +0,0 @@
|
||||
# 我的页面背景图占位文件
|
||||
# 实际使用时请替换为对应的背景图片文件
|
||||
@@ -1,2 +0,0 @@
|
||||
# 首页激活图标占位文件
|
||||
# 实际使用时请替换为对应的图标文件
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" fill="rgb(93, 135, 255)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 184 B |
@@ -1,2 +0,0 @@
|
||||
# 首页图标占位文件
|
||||
# 实际使用时请替换为对应的图标文件
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" fill="#999999"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -1,2 +0,0 @@
|
||||
# 我的激活图标占位文件
|
||||
# 实际使用时请替换为对应的图标文件
|
||||
@@ -1,2 +0,0 @@
|
||||
# 我的图标占位文件
|
||||
# 实际使用时请替换为对应的图标文件
|
||||
@@ -0,0 +1,5 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 256 B |
@@ -0,0 +1,5 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 236 B |
@@ -1,2 +0,0 @@
|
||||
# 工作台激活图标占位文件
|
||||
# 实际使用时请替换为对应的图标文件
|
||||
@@ -0,0 +1,5 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 319 B |
@@ -1,2 +0,0 @@
|
||||
# 工作台图标占位文件
|
||||
# 实际使用时请替换为对应的图标文件
|
||||
@@ -0,0 +1,5 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 299 B |
@@ -1,42 +1,303 @@
|
||||
// uni.scss 全局样式变量
|
||||
|
||||
// 主题色彩
|
||||
$uni-primary: #3c9cff;
|
||||
$uni-success: #5ac725;
|
||||
$uni-warning: #f9ae3d;
|
||||
$uni-error: #f56c6c;
|
||||
$uni-info: #909399;
|
||||
|
||||
// 辅助色彩
|
||||
$uni-main-color: #303133;
|
||||
$uni-base-color: #606266;
|
||||
$uni-secondary-color: #909399;
|
||||
$uni-placeholder-color: #C0C4CC;
|
||||
|
||||
// 背景色
|
||||
$uni-bg-color: #f5f5f5;
|
||||
$uni-bg-color-grey: #f8f8f8;
|
||||
$uni-bg-color-hover: #f1f1f1;
|
||||
$uni-bg-color-mask: rgba(0, 0, 0, 0.4);
|
||||
|
||||
// 边框色
|
||||
$uni-border-1: #DCDFE6;
|
||||
$uni-border-2: #E4E7ED;
|
||||
$uni-border-3: #EBEEF5;
|
||||
$uni-border-4: #F2F6FC;
|
||||
|
||||
// 尺寸变量
|
||||
$uni-border-radius-sm: 4rpx;
|
||||
$uni-border-radius-base: 6rpx;
|
||||
$uni-border-radius-lg: 12rpx;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
// 字体大小
|
||||
$uni-font-size-sm: 24rpx;
|
||||
$uni-font-size-base: 28rpx;
|
||||
$uni-font-size-lg: 32rpx;
|
||||
|
||||
// 间距
|
||||
$uni-spacing-sm: 16rpx;
|
||||
$uni-spacing-base: 24rpx;
|
||||
$uni-spacing-lg: 32rpx;
|
||||
/**
|
||||
* 这里是uni-app内置的常用样式变量
|
||||
*
|
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||
*
|
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||
*/
|
||||
|
||||
// 引入 uview-plus 样式
|
||||
@import 'uview-plus/theme.scss';
|
||||
|
||||
/* 原有的Sass变量定义 - 保持向后兼容 */
|
||||
$uni-color-primary: rgb(93, 135, 255);
|
||||
$uni-color-primary-dark: rgb(73, 115, 235);
|
||||
$uni-color-primary-light: rgba(93, 135, 255, 0.1);
|
||||
|
||||
/* 状态颜色 */
|
||||
$uni-color-success: #52c41a;
|
||||
$uni-color-warning: #faad14;
|
||||
$uni-color-error: #ff4d4f;
|
||||
$uni-color-info: #1890ff;
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color: #333333;
|
||||
$uni-text-color-inverse: #fff;
|
||||
$uni-text-color-secondary: #666666;
|
||||
$uni-text-color-grey: #999999;
|
||||
$uni-text-color-placeholder: #c0c0c0;
|
||||
$uni-text-color-disable: #d9d9d9;
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color: #ffffff;
|
||||
$uni-bg-color-grey: #f8f9fa;
|
||||
$uni-bg-color-hover: #f1f1f1;
|
||||
$uni-bg-color-mask: rgba(0, 0, 0, 0.4);
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color: #e0e0e0;
|
||||
|
||||
/* 阴影 */
|
||||
$uni-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
$uni-shadow-hover: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm: 12px;
|
||||
$uni-font-size-base: 14px;
|
||||
$uni-font-size-lg: 16px;
|
||||
$uni-font-size-xl: 18px;
|
||||
$uni-font-size-xxl: 20px;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm: 20px;
|
||||
$uni-img-size-base: 26px;
|
||||
$uni-img-size-lg: 40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 6px;
|
||||
$uni-border-radius-base: 8px;
|
||||
$uni-border-radius-lg: 12px;
|
||||
$uni-border-radius-xl: 16px;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
/* 间距 */
|
||||
$uni-spacing-row-sm: 8px;
|
||||
$uni-spacing-row-base: 12px;
|
||||
$uni-spacing-row-lg: 16px;
|
||||
$uni-spacing-row-xl: 20px;
|
||||
|
||||
$uni-spacing-col-sm: 8px;
|
||||
$uni-spacing-col-base: 12px;
|
||||
$uni-spacing-col-lg: 16px;
|
||||
$uni-spacing-col-xl: 20px;
|
||||
|
||||
/* 透明度 */
|
||||
$uni-opacity-disabled: 0.3;
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2c405a;
|
||||
$uni-font-size-title: 20px;
|
||||
$uni-color-subtitle: #555;
|
||||
$uni-font-size-subtitle: 18px;
|
||||
$uni-color-paragraph: #3f536e;
|
||||
$uni-font-size-paragraph: 15px;
|
||||
|
||||
/* 动画 */
|
||||
$uni-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
$uni-transition-fast: all 0.15s ease-out;
|
||||
|
||||
/* 主题色彩变量 - CSS变量版本 */
|
||||
:root {
|
||||
--primary-color: rgb(93, 135, 255);
|
||||
--primary-light: rgba(93, 135, 255, 0.1);
|
||||
--primary-dark: rgb(73, 115, 235);
|
||||
--success-color: #52c41a;
|
||||
--warning-color: #faad14;
|
||||
--error-color: #ff4d4f;
|
||||
--text-primary: #262626;
|
||||
--text-secondary: #595959;
|
||||
--text-disabled: #bfbfbf;
|
||||
--border-color: #f0f0f0;
|
||||
--background-color: #f8f9fa;
|
||||
--white: #ffffff;
|
||||
--shadow-light: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
--shadow-medium: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
--radius-small: 4px;
|
||||
--radius-medium: 8px;
|
||||
--radius-large: 12px;
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
--transition-fast: 0.2s ease;
|
||||
--transition-medium: 0.3s ease;
|
||||
--font-size-xs: 12px;
|
||||
--font-size-sm: 14px;
|
||||
--font-size-md: 16px;
|
||||
--font-size-lg: 18px;
|
||||
--font-size-xl: 20px;
|
||||
}
|
||||
|
||||
/* 全局样式重置 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
color: var(--text-primary);
|
||||
background-color: var(--background-color);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
page {
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 通用工具类 */
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: var(--radius-medium);
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: var(--shadow-light);
|
||||
}
|
||||
|
||||
.mt-sm { margin-top: var(--spacing-sm); }
|
||||
.mt-md { margin-top: var(--spacing-md); }
|
||||
.mt-lg { margin-top: var(--spacing-lg); }
|
||||
.mb-sm { margin-bottom: var(--spacing-sm); }
|
||||
.mb-md { margin-bottom: var(--spacing-md); }
|
||||
.mb-lg { margin-bottom: var(--spacing-lg); }
|
||||
.ml-sm { margin-left: var(--spacing-sm); }
|
||||
.ml-md { margin-left: var(--spacing-md); }
|
||||
.mr-sm { margin-right: var(--spacing-sm); }
|
||||
.mr-md { margin-right: var(--spacing-md); }
|
||||
|
||||
.p-sm { padding: var(--spacing-sm); }
|
||||
.p-md { padding: var(--spacing-md); }
|
||||
.p-lg { padding: var(--spacing-lg); }
|
||||
.px-sm { padding-left: var(--spacing-sm); padding-right: var(--spacing-sm); }
|
||||
.px-md { padding-left: var(--spacing-md); padding-right: var(--spacing-md); }
|
||||
.py-sm { padding-top: var(--spacing-sm); padding-bottom: var(--spacing-sm); }
|
||||
.py-md { padding-top: var(--spacing-md); padding-bottom: var(--spacing-md); }
|
||||
|
||||
/* 按钮基础样式 */
|
||||
.btn {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius-medium);
|
||||
border: none;
|
||||
font-size: var(--font-size-md);
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-dark);
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
.input {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-medium);
|
||||
font-size: var(--font-size-md);
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: var(--primary-color);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(93, 135, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-large);
|
||||
box-shadow: var(--shadow-light);
|
||||
padding: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-md);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 动画类 */
|
||||
.fade-in {
|
||||
animation: fadeIn var(--transition-medium);
|
||||
}
|
||||
|
||||
.slide-up {
|
||||
animation: slideUp var(--transition-medium);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式断点 */
|
||||
@media (max-width: 768px) {
|
||||
.card {
|
||||
margin: var(--spacing-sm);
|
||||
padding: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,776 @@
|
||||
/**
|
||||
* 无障碍访问工具类
|
||||
* 提供屏幕阅读器支持、键盘导航、高对比度模式等功能
|
||||
*/
|
||||
|
||||
class AccessibilityUtils {
|
||||
constructor() {
|
||||
this.isHighContrast = false;
|
||||
this.isLargeText = false;
|
||||
this.isScreenReaderMode = false;
|
||||
this.focusVisible = true;
|
||||
this.currentFocusIndex = 0;
|
||||
this.focusableElements = [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化无障碍功能
|
||||
*/
|
||||
init() {
|
||||
this.setupKeyboardNavigation();
|
||||
this.setupFocusManagement();
|
||||
this.setupScreenReaderSupport();
|
||||
this.loadAccessibilitySettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键盘导航
|
||||
*/
|
||||
setupKeyboardNavigation() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
switch (e.key) {
|
||||
case 'Tab':
|
||||
this.handleTabNavigation(e);
|
||||
break;
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
this.handleActivation(e);
|
||||
break;
|
||||
case 'Escape':
|
||||
this.handleEscape(e);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown':
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowRight':
|
||||
this.handleArrowNavigation(e);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 检测键盘使用,显示焦点指示器
|
||||
document.addEventListener('keydown', () => {
|
||||
this.focusVisible = true;
|
||||
document.body.classList.add('keyboard-navigation');
|
||||
});
|
||||
|
||||
// 检测鼠标使用,隐藏焦点指示器
|
||||
document.addEventListener('mousedown', () => {
|
||||
this.focusVisible = false;
|
||||
document.body.classList.remove('keyboard-navigation');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Tab导航
|
||||
* @param {KeyboardEvent} e - 键盘事件
|
||||
*/
|
||||
handleTabNavigation(e) {
|
||||
this.updateFocusableElements();
|
||||
|
||||
if (this.focusableElements.length === 0) return;
|
||||
|
||||
if (e.shiftKey) {
|
||||
// Shift + Tab - 反向导航
|
||||
this.currentFocusIndex =
|
||||
this.currentFocusIndex <= 0
|
||||
? this.focusableElements.length - 1
|
||||
: this.currentFocusIndex - 1;
|
||||
} else {
|
||||
// Tab - 正向导航
|
||||
this.currentFocusIndex =
|
||||
this.currentFocusIndex >= this.focusableElements.length - 1
|
||||
? 0
|
||||
: this.currentFocusIndex + 1;
|
||||
}
|
||||
|
||||
this.focusableElements[this.currentFocusIndex]?.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理激活操作
|
||||
* @param {KeyboardEvent} e - 键盘事件
|
||||
*/
|
||||
handleActivation(e) {
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement && activeElement.click) {
|
||||
activeElement.click();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Escape键
|
||||
* @param {KeyboardEvent} e - 键盘事件
|
||||
*/
|
||||
handleEscape(e) {
|
||||
// 关闭模态框、菜单等
|
||||
const modal = document.querySelector('.modal:not([style*="display: none"])');
|
||||
const menu = document.querySelector('.dropdown-menu.show, .longpress-menu');
|
||||
|
||||
if (modal) {
|
||||
this.closeModal(modal);
|
||||
} else if (menu) {
|
||||
menu.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理方向键导航
|
||||
* @param {KeyboardEvent} e - 键盘事件
|
||||
*/
|
||||
handleArrowNavigation(e) {
|
||||
const currentElement = document.activeElement;
|
||||
const container = currentElement.closest('.grid-layout, .menu-section, .stats-section');
|
||||
|
||||
if (!container) return;
|
||||
|
||||
const focusableInContainer = this.getFocusableElements(container);
|
||||
const currentIndex = focusableInContainer.indexOf(currentElement);
|
||||
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
let nextIndex = currentIndex;
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const currentRect = currentElement.getBoundingClientRect();
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
nextIndex = this.findElementAbove(focusableInContainer, currentIndex, currentRect);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
nextIndex = this.findElementBelow(focusableInContainer, currentIndex, currentRect);
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
nextIndex = Math.max(0, currentIndex - 1);
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
nextIndex = Math.min(focusableInContainer.length - 1, currentIndex + 1);
|
||||
break;
|
||||
}
|
||||
|
||||
if (nextIndex !== currentIndex) {
|
||||
focusableInContainer[nextIndex]?.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找上方元素
|
||||
*/
|
||||
findElementAbove(elements, currentIndex, currentRect) {
|
||||
let bestIndex = currentIndex;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
if (i === currentIndex) continue;
|
||||
|
||||
const rect = elements[i].getBoundingClientRect();
|
||||
if (rect.bottom <= currentRect.top) {
|
||||
const distance =
|
||||
Math.abs(rect.left - currentRect.left) + (currentRect.top - rect.bottom);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找下方元素
|
||||
*/
|
||||
findElementBelow(elements, currentIndex, currentRect) {
|
||||
let bestIndex = currentIndex;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
if (i === currentIndex) continue;
|
||||
|
||||
const rect = elements[i].getBoundingClientRect();
|
||||
if (rect.top >= currentRect.bottom) {
|
||||
const distance =
|
||||
Math.abs(rect.left - currentRect.left) + (rect.top - currentRect.bottom);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置焦点管理
|
||||
*/
|
||||
setupFocusManagement() {
|
||||
// 焦点指示器样式
|
||||
const focusStyles = `
|
||||
.keyboard-navigation *:focus {
|
||||
outline: 2px solid rgb(93, 135, 255) !important;
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
|
||||
.keyboard-navigation .touchable:focus {
|
||||
outline: 2px solid rgb(93, 135, 255) !important;
|
||||
outline-offset: 2px !important;
|
||||
box-shadow: 0 0 0 4px rgba(93, 135, 255, 0.2) !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.textContent = focusStyles;
|
||||
document.head.appendChild(styleSheet);
|
||||
|
||||
// 焦点陷阱(用于模态框)
|
||||
this.setupFocusTrap();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置焦点陷阱
|
||||
*/
|
||||
setupFocusTrap() {
|
||||
document.addEventListener('focusin', (e) => {
|
||||
const modal = document.querySelector('.modal:not([style*="display: none"])');
|
||||
if (modal && !modal.contains(e.target)) {
|
||||
const firstFocusable = this.getFocusableElements(modal)[0];
|
||||
firstFocusable?.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新可聚焦元素列表
|
||||
*/
|
||||
updateFocusableElements() {
|
||||
this.focusableElements = this.getFocusableElements(document);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可聚焦元素
|
||||
* @param {Element} container - 容器元素
|
||||
* @returns {Element[]} 可聚焦元素数组
|
||||
*/
|
||||
getFocusableElements(container = document) {
|
||||
const focusableSelectors = [
|
||||
'button:not([disabled])',
|
||||
'[href]',
|
||||
'input:not([disabled])',
|
||||
'select:not([disabled])',
|
||||
'textarea:not([disabled])',
|
||||
'[tabindex]:not([tabindex="-1"])',
|
||||
'.touchable',
|
||||
'.service-item',
|
||||
'.menu-item',
|
||||
'.stat-card',
|
||||
].join(', ');
|
||||
|
||||
return Array.from(container.querySelectorAll(focusableSelectors)).filter((el) => {
|
||||
return (
|
||||
el.offsetParent !== null &&
|
||||
!el.hasAttribute('disabled') &&
|
||||
!el.classList.contains('sr-only')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置屏幕阅读器支持
|
||||
*/
|
||||
setupScreenReaderSupport() {
|
||||
// 动态添加ARIA标签
|
||||
this.addAriaLabels();
|
||||
|
||||
// 实时区域更新
|
||||
this.setupLiveRegions();
|
||||
|
||||
// 页面标题更新
|
||||
this.setupPageTitleUpdates();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加ARIA标签
|
||||
*/
|
||||
addAriaLabels() {
|
||||
// 为服务项添加描述
|
||||
document.querySelectorAll('.service-item').forEach((item) => {
|
||||
const title = item.querySelector('.service-title')?.textContent;
|
||||
if (title && !item.hasAttribute('aria-label')) {
|
||||
item.setAttribute('aria-label', `${title}服务`);
|
||||
item.setAttribute('role', 'button');
|
||||
item.setAttribute('tabindex', '0');
|
||||
}
|
||||
});
|
||||
|
||||
// 为统计卡片添加描述
|
||||
document.querySelectorAll('.stat-card').forEach((card) => {
|
||||
const number = card.querySelector('.stat-number')?.textContent;
|
||||
const label = card.querySelector('.stat-label')?.textContent;
|
||||
if (number && label && !card.hasAttribute('aria-label')) {
|
||||
card.setAttribute('aria-label', `${label}:${number}`);
|
||||
card.setAttribute('role', 'button');
|
||||
card.setAttribute('tabindex', '0');
|
||||
}
|
||||
});
|
||||
|
||||
// 为导航按钮添加描述
|
||||
document.querySelectorAll('.nav-tab').forEach((tab) => {
|
||||
const label = tab.querySelector('.nav-label')?.textContent;
|
||||
if (label && !tab.hasAttribute('aria-label')) {
|
||||
tab.setAttribute('aria-label', `导航到${label}页面`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置实时区域
|
||||
*/
|
||||
setupLiveRegions() {
|
||||
// 创建实时区域用于通知
|
||||
const liveRegion = document.createElement('div');
|
||||
liveRegion.setAttribute('aria-live', 'polite');
|
||||
liveRegion.setAttribute('aria-atomic', 'true');
|
||||
liveRegion.className = 'sr-only';
|
||||
liveRegion.id = 'live-region';
|
||||
document.body.appendChild(liveRegion);
|
||||
|
||||
// 监听toast消息
|
||||
const originalShowToast = uni.showToast;
|
||||
uni.showToast = (options) => {
|
||||
this.announceToScreenReader(options.title || options.content);
|
||||
return originalShowToast(options);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 向屏幕阅读器宣布消息
|
||||
* @param {string} message - 消息内容
|
||||
*/
|
||||
announceToScreenReader(message) {
|
||||
const liveRegion = document.getElementById('live-region');
|
||||
if (liveRegion) {
|
||||
liveRegion.textContent = message;
|
||||
|
||||
// 清空内容以便下次宣布
|
||||
setTimeout(() => {
|
||||
liveRegion.textContent = '';
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置页面标题更新
|
||||
*/
|
||||
setupPageTitleUpdates() {
|
||||
// 监听路由变化,更新页面标题
|
||||
const originalSwitchTab = uni.switchTab;
|
||||
uni.switchTab = (options) => {
|
||||
const pageTitles = {
|
||||
'/pages/index/index': '智慧校园 - 首页',
|
||||
'/pages/workbench/index': '智慧校园 - 办公中心',
|
||||
'/pages/profile/index': '智慧校园 - 我的',
|
||||
};
|
||||
|
||||
const title = pageTitles[options.url];
|
||||
if (title) {
|
||||
document.title = title;
|
||||
this.announceToScreenReader(`已切换到${title.split(' - ')[1]}页面`);
|
||||
}
|
||||
|
||||
return originalSwitchTab(options);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用高对比度模式
|
||||
*/
|
||||
enableHighContrast() {
|
||||
this.isHighContrast = true;
|
||||
document.body.classList.add('high-contrast');
|
||||
this.saveAccessibilitySettings();
|
||||
this.announceToScreenReader('已启用高对比度模式');
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用高对比度模式
|
||||
*/
|
||||
disableHighContrast() {
|
||||
this.isHighContrast = false;
|
||||
document.body.classList.remove('high-contrast');
|
||||
this.saveAccessibilitySettings();
|
||||
this.announceToScreenReader('已禁用高对比度模式');
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用大字体模式
|
||||
*/
|
||||
enableLargeText() {
|
||||
this.isLargeText = true;
|
||||
document.body.classList.add('large-text');
|
||||
this.saveAccessibilitySettings();
|
||||
this.announceToScreenReader('已启用大字体模式');
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用大字体模式
|
||||
*/
|
||||
disableLargeText() {
|
||||
this.isLargeText = false;
|
||||
document.body.classList.remove('large-text');
|
||||
this.saveAccessibilitySettings();
|
||||
this.announceToScreenReader('已禁用大字体模式');
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用屏幕阅读器模式
|
||||
*/
|
||||
enableScreenReaderMode() {
|
||||
this.isScreenReaderMode = true;
|
||||
document.body.classList.add('screen-reader-mode');
|
||||
|
||||
// 隐藏装饰性动画
|
||||
const style = document.createElement('style');
|
||||
style.id = 'screen-reader-styles';
|
||||
style.textContent = `
|
||||
.screen-reader-mode * {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
.screen-reader-mode .decorative {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
this.saveAccessibilitySettings();
|
||||
this.announceToScreenReader('已启用屏幕阅读器优化模式');
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用屏幕阅读器模式
|
||||
*/
|
||||
disableScreenReaderMode() {
|
||||
this.isScreenReaderMode = false;
|
||||
document.body.classList.remove('screen-reader-mode');
|
||||
|
||||
const style = document.getElementById('screen-reader-styles');
|
||||
if (style) style.remove();
|
||||
|
||||
this.saveAccessibilitySettings();
|
||||
this.announceToScreenReader('已禁用屏幕阅读器优化模式');
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存无障碍设置
|
||||
*/
|
||||
saveAccessibilitySettings() {
|
||||
const settings = {
|
||||
highContrast: this.isHighContrast,
|
||||
largeText: this.isLargeText,
|
||||
screenReaderMode: this.isScreenReaderMode,
|
||||
};
|
||||
|
||||
uni.setStorageSync('accessibility-settings', settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载无障碍设置
|
||||
*/
|
||||
loadAccessibilitySettings() {
|
||||
try {
|
||||
const settings = uni.getStorageSync('accessibility-settings');
|
||||
if (settings) {
|
||||
if (settings.highContrast) this.enableHighContrast();
|
||||
if (settings.largeText) this.enableLargeText();
|
||||
if (settings.screenReaderMode) this.enableScreenReaderMode();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load accessibility settings:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加语义化结构
|
||||
*/
|
||||
addSemanticStructure() {
|
||||
// 添加主要地标
|
||||
const main = document.querySelector('.main-content');
|
||||
if (main && !main.hasAttribute('role')) {
|
||||
main.setAttribute('role', 'main');
|
||||
main.setAttribute('aria-label', '主要内容');
|
||||
}
|
||||
|
||||
// 添加导航地标
|
||||
const nav = document.querySelector('.bottom-nav');
|
||||
if (nav && !nav.hasAttribute('role')) {
|
||||
nav.setAttribute('role', 'navigation');
|
||||
nav.setAttribute('aria-label', '主导航');
|
||||
}
|
||||
|
||||
// 添加区域标识
|
||||
document.querySelectorAll('.card').forEach((card, index) => {
|
||||
if (!card.hasAttribute('role')) {
|
||||
card.setAttribute('role', 'region');
|
||||
const title = card.querySelector('.card-title')?.textContent;
|
||||
if (title) {
|
||||
card.setAttribute('aria-labelledby', `card-title-${index}`);
|
||||
const titleElement = card.querySelector('.card-title');
|
||||
if (titleElement) {
|
||||
titleElement.id = `card-title-${index}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭模态框
|
||||
* @param {Element} modal - 模态框元素
|
||||
*/
|
||||
closeModal(modal) {
|
||||
modal.style.display = 'none';
|
||||
|
||||
// 恢复焦点到触发元素
|
||||
const trigger = document.querySelector('[data-modal-trigger]');
|
||||
if (trigger) {
|
||||
trigger.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应式设计工具类
|
||||
*/
|
||||
class ResponsiveUtils {
|
||||
constructor() {
|
||||
this.breakpoints = {
|
||||
mobile: 375,
|
||||
tablet: 768,
|
||||
desktop: 1024,
|
||||
};
|
||||
|
||||
this.currentBreakpoint = this.getCurrentBreakpoint();
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化响应式功能
|
||||
*/
|
||||
init() {
|
||||
this.setupViewportMeta();
|
||||
this.setupMediaQueries();
|
||||
this.setupOrientationChange();
|
||||
this.addResponsiveClasses();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置viewport元标签
|
||||
*/
|
||||
setupViewportMeta() {
|
||||
let viewport = document.querySelector('meta[name="viewport"]');
|
||||
if (!viewport) {
|
||||
viewport = document.createElement('meta');
|
||||
viewport.name = 'viewport';
|
||||
document.head.appendChild(viewport);
|
||||
}
|
||||
viewport.content =
|
||||
'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置媒体查询监听
|
||||
*/
|
||||
setupMediaQueries() {
|
||||
Object.entries(this.breakpoints).forEach(([name, width]) => {
|
||||
const mediaQuery = window.matchMedia(`(max-width: ${width}px)`);
|
||||
|
||||
mediaQuery.addListener((e) => {
|
||||
this.handleBreakpointChange();
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.handleBreakpointChange();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理断点变化
|
||||
*/
|
||||
handleBreakpointChange() {
|
||||
const newBreakpoint = this.getCurrentBreakpoint();
|
||||
|
||||
if (newBreakpoint !== this.currentBreakpoint) {
|
||||
this.currentBreakpoint = newBreakpoint;
|
||||
this.addResponsiveClasses();
|
||||
|
||||
// 触发自定义事件
|
||||
const event = new CustomEvent('breakpointChange', {
|
||||
detail: { breakpoint: newBreakpoint },
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前断点
|
||||
*/
|
||||
getCurrentBreakpoint() {
|
||||
const width = window.innerWidth;
|
||||
|
||||
if (width <= this.breakpoints.mobile) return 'mobile';
|
||||
if (width <= this.breakpoints.tablet) return 'tablet';
|
||||
return 'desktop';
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加响应式类名
|
||||
*/
|
||||
addResponsiveClasses() {
|
||||
document.body.className = document.body.className
|
||||
.replace(/\b(mobile|tablet|desktop)\b/g, '')
|
||||
.trim();
|
||||
|
||||
document.body.classList.add(this.currentBreakpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置屏幕方向变化监听
|
||||
*/
|
||||
setupOrientationChange() {
|
||||
window.addEventListener('orientationchange', () => {
|
||||
setTimeout(() => {
|
||||
this.handleBreakpointChange();
|
||||
|
||||
// 修复iOS Safari的viewport问题
|
||||
if (/iPhone|iPad/.test(navigator.userAgent)) {
|
||||
document.body.style.height = window.innerHeight + 'px';
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取安全区域内边距
|
||||
*/
|
||||
getSafeAreaInsets() {
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
|
||||
return {
|
||||
top: computedStyle.getPropertyValue('env(safe-area-inset-top)') || '0px',
|
||||
right: computedStyle.getPropertyValue('env(safe-area-inset-right)') || '0px',
|
||||
bottom: computedStyle.getPropertyValue('env(safe-area-inset-bottom)') || '0px',
|
||||
left: computedStyle.getPropertyValue('env(safe-area-inset-left)') || '0px',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为移动设备
|
||||
*/
|
||||
isMobile() {
|
||||
return this.currentBreakpoint === 'mobile';
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为平板设备
|
||||
*/
|
||||
isTablet() {
|
||||
return this.currentBreakpoint === 'tablet';
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为桌面设备
|
||||
*/
|
||||
isDesktop() {
|
||||
return this.currentBreakpoint === 'desktop';
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局实例
|
||||
const accessibilityUtils = new AccessibilityUtils();
|
||||
const responsiveUtils = new ResponsiveUtils();
|
||||
|
||||
// 添加全局样式
|
||||
const globalStyles = `
|
||||
/* 高对比度模式 */
|
||||
.high-contrast {
|
||||
--uni-color-primary: #000000;
|
||||
--uni-bg-color: #ffffff;
|
||||
--uni-text-color: #000000;
|
||||
--uni-text-color-secondary: #333333;
|
||||
--uni-border-color: #000000;
|
||||
}
|
||||
|
||||
.high-contrast .card {
|
||||
border: 2px solid #000000;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* 大字体模式 */
|
||||
.large-text {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.large-text .card-title {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.large-text .service-title {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
/* 屏幕阅读器模式 */
|
||||
.screen-reader-mode .decorative {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 键盘导航焦点样式 */
|
||||
.keyboard-navigation *:focus {
|
||||
outline: 2px solid rgb(93, 135, 255);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 375px) {
|
||||
.mobile .grid-4 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.mobile .service-item {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.mobile .card {
|
||||
margin: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 320px) {
|
||||
.mobile .grid-4,
|
||||
.mobile .grid-3 {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 安全区域适配 */
|
||||
.page-container {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
}
|
||||
`;
|
||||
|
||||
// 插入全局样式
|
||||
const globalStyleSheet = document.createElement('style');
|
||||
globalStyleSheet.textContent = globalStyles;
|
||||
document.head.appendChild(globalStyleSheet);
|
||||
|
||||
export { AccessibilityUtils, ResponsiveUtils };
|
||||
export default { accessibilityUtils, responsiveUtils };
|
||||
@@ -0,0 +1,422 @@
|
||||
/**
|
||||
* 动画工具类
|
||||
* 提供页面转场、组件动画和微交互效果的统一管理
|
||||
*/
|
||||
|
||||
class AnimationUtils {
|
||||
constructor() {
|
||||
this.animationDuration = 300;
|
||||
this.easingFunction = 'cubic-bezier(0.4, 0, 0.2, 1)';
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面进入动画
|
||||
* @param {string} selector - 选择器
|
||||
* @param {string} type - 动画类型
|
||||
*/
|
||||
pageEnter(selector = '.page-container', type = 'fadeInUp') {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
element.style.animation = `${type} 0.6s ease-out`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面退出动画
|
||||
* @param {string} selector - 选择器
|
||||
* @param {Function} callback - 回调函数
|
||||
*/
|
||||
pageExit(selector = '.page-container', callback) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
element.style.animation = 'fadeOutDown 0.3s ease-in forwards';
|
||||
setTimeout(() => {
|
||||
if (callback) callback();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡片进入动画(错开显示)
|
||||
* @param {string} selector - 选择器
|
||||
*/
|
||||
staggerCards(selector = '.card, .service-item, .stat-card') {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
elements.forEach((element, index) => {
|
||||
element.style.animationDelay = `${index * 0.1}s`;
|
||||
element.style.animation = 'slideInUp 0.6s ease-out backwards';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表项动画
|
||||
* @param {string} selector - 选择器
|
||||
*/
|
||||
animateList(selector = '.menu-item, .todo-item, .news-item') {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
elements.forEach((element, index) => {
|
||||
element.style.animationDelay = `${index * 0.05}s`;
|
||||
element.style.animation = 'slideInRight 0.4s ease-out backwards';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击反馈动画
|
||||
* @param {HTMLElement} element - 元素
|
||||
*/
|
||||
clickFeedback(element) {
|
||||
if (!element) return;
|
||||
|
||||
element.style.transform = 'scale(0.95)';
|
||||
element.style.transition = 'transform 0.1s ease-out';
|
||||
|
||||
setTimeout(() => {
|
||||
element.style.transform = 'scale(1)';
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载动画
|
||||
* @param {string} selector - 选择器
|
||||
*/
|
||||
showLoading(selector = 'body') {
|
||||
const loadingHtml = `
|
||||
<div class="loading-overlay" style="
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
">
|
||||
<div class="spinner" style="
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #f0f0f0;
|
||||
border-top: 3px solid rgb(93, 135, 255);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const container = document.querySelector(selector);
|
||||
if (container) {
|
||||
container.insertAdjacentHTML('beforeend', loadingHtml);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏加载动画
|
||||
*/
|
||||
hideLoading() {
|
||||
const loading = document.querySelector('.loading-overlay');
|
||||
if (loading) {
|
||||
loading.style.animation = 'fadeOut 0.3s ease-in forwards';
|
||||
setTimeout(() => {
|
||||
loading.remove();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast 提示动画
|
||||
* @param {string} message - 消息内容
|
||||
* @param {number} duration - 持续时间
|
||||
*/
|
||||
showToast(message, duration = 2000) {
|
||||
// 移除已存在的toast
|
||||
const existingToast = document.querySelector('.animated-toast');
|
||||
if (existingToast) {
|
||||
existingToast.remove();
|
||||
}
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'animated-toast';
|
||||
toast.textContent = message;
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 25px;
|
||||
z-index: 10000;
|
||||
font-size: 14px;
|
||||
max-width: 280px;
|
||||
text-align: center;
|
||||
animation: slideInDown 0.3s ease-out;
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// 自动移除
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOutUp 0.3s ease-in forwards';
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.remove();
|
||||
}
|
||||
}, 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计数字计数动画
|
||||
* @param {HTMLElement} element - 元素
|
||||
* @param {number} endValue - 结束值
|
||||
* @param {number} duration - 持续时间
|
||||
*/
|
||||
countUp(element, endValue, duration = 1000) {
|
||||
if (!element) return;
|
||||
|
||||
const startValue = 0;
|
||||
const startTime = Date.now();
|
||||
|
||||
const animate = () => {
|
||||
const currentTime = Date.now();
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// 使用缓动函数
|
||||
const easeProgress = 1 - Math.pow(1 - progress, 3);
|
||||
const currentValue = startValue + (endValue - startValue) * easeProgress;
|
||||
|
||||
// 格式化显示
|
||||
if (endValue % 1 === 0) {
|
||||
element.textContent = Math.round(currentValue);
|
||||
} else {
|
||||
element.textContent = currentValue.toFixed(1);
|
||||
}
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
animate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 下拉刷新动画
|
||||
* @param {HTMLElement} element - 元素
|
||||
*/
|
||||
pullToRefresh(element) {
|
||||
if (!element) return;
|
||||
|
||||
element.style.transform = 'translateY(50px)';
|
||||
element.style.transition = 'transform 0.3s ease-out';
|
||||
|
||||
setTimeout(() => {
|
||||
element.style.transform = 'translateY(0)';
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索聚焦动画
|
||||
* @param {HTMLElement} searchBar - 搜索栏元素
|
||||
*/
|
||||
searchFocus(searchBar) {
|
||||
if (!searchBar) return;
|
||||
|
||||
searchBar.style.transform = 'translateY(-2px)';
|
||||
searchBar.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.15)';
|
||||
searchBar.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索失焦动画
|
||||
* @param {HTMLElement} searchBar - 搜索栏元素
|
||||
*/
|
||||
searchBlur(searchBar) {
|
||||
if (!searchBar) return;
|
||||
|
||||
searchBar.style.transform = 'translateY(0)';
|
||||
searchBar.style.boxShadow = '0 2px 12px rgba(0, 0, 0, 0.1)';
|
||||
}
|
||||
|
||||
/**
|
||||
* 模态框显示动画
|
||||
* @param {HTMLElement} modal - 模态框元素
|
||||
*/
|
||||
showModal(modal) {
|
||||
if (!modal) return;
|
||||
|
||||
modal.style.display = 'flex';
|
||||
modal.style.animation = 'fadeIn 0.3s ease-out';
|
||||
|
||||
const content = modal.querySelector('.modal-content');
|
||||
if (content) {
|
||||
content.style.animation = 'slideInUp 0.3s ease-out';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模态框隐藏动画
|
||||
* @param {HTMLElement} modal - 模态框元素
|
||||
* @param {Function} callback - 回调函数
|
||||
*/
|
||||
hideModal(modal, callback) {
|
||||
if (!modal) return;
|
||||
|
||||
modal.style.animation = 'fadeOut 0.3s ease-in forwards';
|
||||
|
||||
const content = modal.querySelector('.modal-content');
|
||||
if (content) {
|
||||
content.style.animation = 'slideOutDown 0.3s ease-in forwards';
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
modal.style.display = 'none';
|
||||
if (callback) callback();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局实例
|
||||
const animationUtils = new AnimationUtils();
|
||||
|
||||
// 定义CSS动画关键帧
|
||||
const animationStyles = `
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOutDown {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutDown {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutUp {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.3);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
70% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(5px); }
|
||||
}
|
||||
`;
|
||||
|
||||
// 插入动画样式到页面
|
||||
if (typeof document !== 'undefined') {
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.textContent = animationStyles;
|
||||
document.head.appendChild(styleSheet);
|
||||
}
|
||||
|
||||
export default animationUtils;
|
||||
export { AnimationUtils };
|
||||
@@ -1,127 +0,0 @@
|
||||
/**
|
||||
* 认证工具类
|
||||
*/
|
||||
|
||||
const TOKEN_KEY = 'token';
|
||||
const USER_INFO_KEY = 'userInfo';
|
||||
|
||||
/**
|
||||
* 获取Token
|
||||
*/
|
||||
export function getToken() {
|
||||
return uni.getStorageSync(TOKEN_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Token
|
||||
*/
|
||||
export function setToken(token) {
|
||||
return uni.setStorageSync(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除Token
|
||||
*/
|
||||
export function removeToken() {
|
||||
return uni.removeStorageSync(TOKEN_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
export function getUserInfo() {
|
||||
return uni.getStorageSync(USER_INFO_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户信息
|
||||
*/
|
||||
export function setUserInfo(userInfo) {
|
||||
return uni.setStorageSync(USER_INFO_KEY, userInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除用户信息
|
||||
*/
|
||||
export function removeUserInfo() {
|
||||
return uni.removeStorageSync(USER_INFO_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已登录
|
||||
*/
|
||||
export function isLoggedIn() {
|
||||
const token = getToken();
|
||||
return !!token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有认证信息
|
||||
*/
|
||||
export function clearAuth() {
|
||||
removeToken();
|
||||
removeUserInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
export function login(loginData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 模拟登录API调用
|
||||
setTimeout(() => {
|
||||
if (loginData.username === 'admin' && loginData.password === 'admin') {
|
||||
const token = 'mock-token-' + Date.now();
|
||||
const userInfo = {
|
||||
id: 1,
|
||||
username: loginData.username,
|
||||
nickname: '管理员',
|
||||
avatar: '/static/avatar.png',
|
||||
title: '系统管理员',
|
||||
};
|
||||
|
||||
setToken(token);
|
||||
setUserInfo(userInfo);
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
token,
|
||||
userInfo,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
reject(new Error('用户名或密码错误'));
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
export function logout() {
|
||||
return new Promise((resolve) => {
|
||||
clearAuth();
|
||||
|
||||
// 跳转到登录页
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/login',
|
||||
});
|
||||
|
||||
resolve({ success: true });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录状态并跳转
|
||||
*/
|
||||
export function checkAuth() {
|
||||
if (!isLoggedIn()) {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/login',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// 设备信息和兼容性工具
|
||||
export const deviceUtils = {
|
||||
// 获取系统信息
|
||||
getSystemInfo() {
|
||||
try {
|
||||
return uni.getSystemInfoSync()
|
||||
} catch (e) {
|
||||
console.error('获取系统信息失败:', e)
|
||||
return {}
|
||||
}
|
||||
},
|
||||
|
||||
// 判断是否为低端设备
|
||||
isLowEndDevice() {
|
||||
const systemInfo = this.getSystemInfo()
|
||||
|
||||
// 判断条件:内存小于4GB 或 CPU性能较差
|
||||
if (systemInfo.memSize && systemInfo.memSize < 4) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Android 设备版本较低
|
||||
if (systemInfo.platform === 'android' && systemInfo.system) {
|
||||
const version = parseFloat(systemInfo.system.replace('Android ', ''))
|
||||
if (version < 7.0) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// iOS 设备版本较低
|
||||
if (systemInfo.platform === 'ios' && systemInfo.system) {
|
||||
const version = parseFloat(systemInfo.system.replace('iOS ', ''))
|
||||
if (version < 12.0) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
// 获取屏幕适配信息
|
||||
getScreenInfo() {
|
||||
const systemInfo = this.getSystemInfo()
|
||||
|
||||
return {
|
||||
screenWidth: systemInfo.screenWidth,
|
||||
screenHeight: systemInfo.screenHeight,
|
||||
statusBarHeight: systemInfo.statusBarHeight,
|
||||
safeAreaInsets: systemInfo.safeAreaInsets,
|
||||
// 判断是否为刘海屏
|
||||
hasNotch:
|
||||
systemInfo.safeAreaInsets &&
|
||||
systemInfo.safeAreaInsets.top > systemInfo.statusBarHeight,
|
||||
// 判断是否为横屏
|
||||
isLandscape: systemInfo.screenWidth > systemInfo.screenHeight,
|
||||
}
|
||||
},
|
||||
|
||||
// 获取设备类型
|
||||
getDeviceType() {
|
||||
const systemInfo = this.getSystemInfo()
|
||||
|
||||
if (systemInfo.uniPlatform === 'mp-weixin') {
|
||||
return 'wechat-miniprogram'
|
||||
} else if (systemInfo.uniPlatform === 'h5') {
|
||||
return 'h5'
|
||||
} else if (systemInfo.uniPlatform === 'app-plus') {
|
||||
return systemInfo.platform === 'ios' ? 'ios-app' : 'android-app'
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
},
|
||||
}
|
||||
|
||||
// 网络状态管理
|
||||
export const networkUtils = {
|
||||
// 获取网络状态
|
||||
getNetworkType() {
|
||||
return new Promise((resolve) => {
|
||||
uni.getNetworkType({
|
||||
success: (res) => {
|
||||
resolve(res.networkType)
|
||||
},
|
||||
fail: () => {
|
||||
resolve('unknown')
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 监听网络状态变化
|
||||
onNetworkStatusChange(callback) {
|
||||
uni.onNetworkStatusChange(callback)
|
||||
},
|
||||
|
||||
// 判断是否为弱网环境
|
||||
async isWeakNetwork() {
|
||||
const networkType = await this.getNetworkType()
|
||||
return ['2g', '3g', 'slow'].includes(networkType)
|
||||
},
|
||||
|
||||
// 根据网络状况调整请求策略
|
||||
async getRequestConfig() {
|
||||
const isWeak = await this.isWeakNetwork()
|
||||
|
||||
if (isWeak) {
|
||||
return {
|
||||
timeout: 30000, // 增加超时时间
|
||||
retryTimes: 3, // 增加重试次数
|
||||
useCache: true, // 优先使用缓存
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
timeout: 10000,
|
||||
retryTimes: 1,
|
||||
useCache: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// 性能优化工具
|
||||
export const performanceUtils = {
|
||||
// 图片懒加载配置
|
||||
getImageLoadConfig() {
|
||||
const isLowEnd = deviceUtils.isLowEndDevice()
|
||||
|
||||
return {
|
||||
// 低端设备减少同时加载的图片数量
|
||||
concurrent: isLowEnd ? 2 : 5,
|
||||
// 低端设备使用较小的图片
|
||||
quality: isLowEnd ? 'low' : 'high',
|
||||
// 预加载距离
|
||||
threshold: isLowEnd ? 100 : 300,
|
||||
}
|
||||
},
|
||||
|
||||
// 动画性能配置
|
||||
getAnimationConfig() {
|
||||
const isLowEnd = deviceUtils.isLowEndDevice()
|
||||
|
||||
return {
|
||||
// 低端设备禁用复杂动画
|
||||
enableComplexAnimation: !isLowEnd,
|
||||
// 动画持续时间
|
||||
duration: isLowEnd ? 200 : 300,
|
||||
// 动画缓动函数
|
||||
easing: isLowEnd ? 'linear' : 'ease-out',
|
||||
}
|
||||
},
|
||||
|
||||
// 列表滚动优化
|
||||
getScrollConfig() {
|
||||
const isLowEnd = deviceUtils.isLowEndDevice()
|
||||
|
||||
return {
|
||||
// 虚拟滚动阈值
|
||||
virtualScrollThreshold: isLowEnd ? 50 : 100,
|
||||
// 缓冲区大小
|
||||
bufferSize: isLowEnd ? 5 : 10,
|
||||
// 是否开启上拉加载节流
|
||||
enableThrottle: isLowEnd,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// 适配工具
|
||||
export const adaptUtils = {
|
||||
// 获取安全区域样式
|
||||
getSafeAreaStyle() {
|
||||
const screenInfo = deviceUtils.getScreenInfo()
|
||||
|
||||
return {
|
||||
paddingTop: `${screenInfo.safeAreaInsets?.top || screenInfo.statusBarHeight || 0}px`,
|
||||
paddingBottom: `${screenInfo.safeAreaInsets?.bottom || 0}px`,
|
||||
paddingLeft: `${screenInfo.safeAreaInsets?.left || 0}px`,
|
||||
paddingRight: `${screenInfo.safeAreaInsets?.right || 0}px`,
|
||||
}
|
||||
},
|
||||
|
||||
// 获取底部安全区域高度
|
||||
getBottomSafeHeight() {
|
||||
const screenInfo = deviceUtils.getScreenInfo()
|
||||
return screenInfo.safeAreaInsets?.bottom || 0
|
||||
},
|
||||
|
||||
// 获取顶部安全区域高度
|
||||
getTopSafeHeight() {
|
||||
const screenInfo = deviceUtils.getScreenInfo()
|
||||
return screenInfo.safeAreaInsets?.top || screenInfo.statusBarHeight || 0
|
||||
},
|
||||
|
||||
// rpx转px
|
||||
rpxToPx(rpx) {
|
||||
const systemInfo = deviceUtils.getSystemInfo()
|
||||
return (rpx * systemInfo.screenWidth) / 750
|
||||
},
|
||||
|
||||
// px转rpx
|
||||
pxToRpx(px) {
|
||||
const systemInfo = deviceUtils.getSystemInfo()
|
||||
return (px * 750) / systemInfo.screenWidth
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
/**
|
||||
* 手势识别工具类
|
||||
* 支持滑动、长按、双击、捏合缩放等手势操作
|
||||
*/
|
||||
|
||||
class GestureUtils {
|
||||
constructor() {
|
||||
this.touchStartTime = 0;
|
||||
this.touchStartPos = { x: 0, y: 0 };
|
||||
this.touchMovePos = { x: 0, y: 0 };
|
||||
this.lastTouchTime = 0;
|
||||
this.tapCount = 0;
|
||||
this.longPressTimer = null;
|
||||
this.longPressDelay = 500; // 长按延迟时间
|
||||
this.doubleTapDelay = 300; // 双击延迟时间
|
||||
this.swipeThreshold = 50; // 滑动阈值
|
||||
this.gestureCallbacks = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化手势监听
|
||||
* @param {HTMLElement} element - 监听元素
|
||||
* @param {Object} options - 配置选项
|
||||
*/
|
||||
init(element, options = {}) {
|
||||
if (!element) return;
|
||||
|
||||
const config = {
|
||||
enableSwipe: true,
|
||||
enableLongPress: true,
|
||||
enableDoubleTap: true,
|
||||
enablePinch: false,
|
||||
...options,
|
||||
};
|
||||
|
||||
// 存储元素和配置
|
||||
this.element = element;
|
||||
this.config = config;
|
||||
|
||||
// 绑定事件监听器
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定触摸事件
|
||||
*/
|
||||
bindEvents() {
|
||||
if (!this.element) return;
|
||||
|
||||
// 阻止默认的触摸行为
|
||||
this.element.style.touchAction = 'manipulation';
|
||||
|
||||
// 绑定事件
|
||||
this.element.addEventListener('touchstart', this.handleTouchStart.bind(this), {
|
||||
passive: false,
|
||||
});
|
||||
this.element.addEventListener('touchmove', this.handleTouchMove.bind(this), {
|
||||
passive: false,
|
||||
});
|
||||
this.element.addEventListener('touchend', this.handleTouchEnd.bind(this), {
|
||||
passive: false,
|
||||
});
|
||||
this.element.addEventListener('touchcancel', this.handleTouchCancel.bind(this), {
|
||||
passive: false,
|
||||
});
|
||||
|
||||
// 绑定鼠标事件(用于桌面端测试)
|
||||
this.element.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
||||
this.element.addEventListener('mousemove', this.handleMouseMove.bind(this));
|
||||
this.element.addEventListener('mouseup', this.handleMouseUp.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册手势回调
|
||||
* @param {string} gesture - 手势类型
|
||||
* @param {Function} callback - 回调函数
|
||||
*/
|
||||
on(gesture, callback) {
|
||||
if (!this.gestureCallbacks.has(gesture)) {
|
||||
this.gestureCallbacks.set(gesture, []);
|
||||
}
|
||||
this.gestureCallbacks.get(gesture).push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发手势回调
|
||||
* @param {string} gesture - 手势类型
|
||||
* @param {Object} data - 手势数据
|
||||
*/
|
||||
trigger(gesture, data = {}) {
|
||||
const callbacks = this.gestureCallbacks.get(gesture);
|
||||
if (callbacks) {
|
||||
callbacks.forEach((callback) => callback(data));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理触摸开始
|
||||
* @param {TouchEvent} e - 触摸事件
|
||||
*/
|
||||
handleTouchStart(e) {
|
||||
const touch = e.touches[0];
|
||||
this.touchStartTime = Date.now();
|
||||
this.touchStartPos = { x: touch.clientX, y: touch.clientY };
|
||||
this.touchMovePos = { x: touch.clientX, y: touch.clientY };
|
||||
|
||||
// 长按检测
|
||||
if (this.config.enableLongPress) {
|
||||
this.longPressTimer = setTimeout(() => {
|
||||
this.trigger('longpress', {
|
||||
position: this.touchStartPos,
|
||||
target: e.target,
|
||||
});
|
||||
|
||||
// 触觉反馈
|
||||
if (uni && uni.vibrateShort) {
|
||||
uni.vibrateShort();
|
||||
}
|
||||
}, this.longPressDelay);
|
||||
}
|
||||
|
||||
// 双击检测
|
||||
if (this.config.enableDoubleTap) {
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - this.lastTouchTime < this.doubleTapDelay) {
|
||||
this.tapCount++;
|
||||
} else {
|
||||
this.tapCount = 1;
|
||||
}
|
||||
this.lastTouchTime = currentTime;
|
||||
}
|
||||
|
||||
// 阻止默认行为(可选)
|
||||
if (this.config.preventDefault !== false) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理触摸移动
|
||||
* @param {TouchEvent} e - 触摸事件
|
||||
*/
|
||||
handleTouchMove(e) {
|
||||
const touch = e.touches[0];
|
||||
this.touchMovePos = { x: touch.clientX, y: touch.clientY };
|
||||
|
||||
// 如果移动了,取消长按
|
||||
if (this.longPressTimer) {
|
||||
const distance = this.getDistance(this.touchStartPos, this.touchMovePos);
|
||||
if (distance > 10) {
|
||||
clearTimeout(this.longPressTimer);
|
||||
this.longPressTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 滑动检测
|
||||
if (this.config.enableSwipe) {
|
||||
const deltaX = this.touchMovePos.x - this.touchStartPos.x;
|
||||
const deltaY = this.touchMovePos.y - this.touchStartPos.y;
|
||||
|
||||
this.trigger('swipemove', {
|
||||
deltaX,
|
||||
deltaY,
|
||||
startPos: this.touchStartPos,
|
||||
currentPos: this.touchMovePos,
|
||||
target: e.target,
|
||||
});
|
||||
}
|
||||
|
||||
// 捏合缩放检测
|
||||
if (this.config.enablePinch && e.touches.length === 2) {
|
||||
this.handlePinch(e);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理触摸结束
|
||||
* @param {TouchEvent} e - 触摸事件
|
||||
*/
|
||||
handleTouchEnd(e) {
|
||||
const touchEndTime = Date.now();
|
||||
const touchDuration = touchEndTime - this.touchStartTime;
|
||||
|
||||
// 清除长按定时器
|
||||
if (this.longPressTimer) {
|
||||
clearTimeout(this.longPressTimer);
|
||||
this.longPressTimer = null;
|
||||
}
|
||||
|
||||
// 滑动检测
|
||||
if (this.config.enableSwipe) {
|
||||
this.detectSwipe();
|
||||
}
|
||||
|
||||
// 点击和双击检测
|
||||
if (this.config.enableDoubleTap && touchDuration < 300) {
|
||||
setTimeout(() => {
|
||||
if (this.tapCount === 1) {
|
||||
this.trigger('tap', {
|
||||
position: this.touchStartPos,
|
||||
target: e.target,
|
||||
});
|
||||
} else if (this.tapCount === 2) {
|
||||
this.trigger('doubletap', {
|
||||
position: this.touchStartPos,
|
||||
target: e.target,
|
||||
});
|
||||
|
||||
// 触觉反馈
|
||||
if (uni && uni.vibrateShort) {
|
||||
uni.vibrateShort();
|
||||
}
|
||||
}
|
||||
this.tapCount = 0;
|
||||
}, this.doubleTapDelay);
|
||||
} else if (touchDuration < 300) {
|
||||
this.trigger('tap', {
|
||||
position: this.touchStartPos,
|
||||
target: e.target,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理触摸取消
|
||||
* @param {TouchEvent} e - 触摸事件
|
||||
*/
|
||||
handleTouchCancel(e) {
|
||||
if (this.longPressTimer) {
|
||||
clearTimeout(this.longPressTimer);
|
||||
this.longPressTimer = null;
|
||||
}
|
||||
this.tapCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测滑动方向和距离
|
||||
*/
|
||||
detectSwipe() {
|
||||
const deltaX = this.touchMovePos.x - this.touchStartPos.x;
|
||||
const deltaY = this.touchMovePos.y - this.touchStartPos.y;
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
|
||||
if (distance < this.swipeThreshold) return;
|
||||
|
||||
const angle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI;
|
||||
let direction = '';
|
||||
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||
// 水平滑动
|
||||
direction = deltaX > 0 ? 'right' : 'left';
|
||||
} else {
|
||||
// 垂直滑动
|
||||
direction = deltaY > 0 ? 'down' : 'up';
|
||||
}
|
||||
|
||||
this.trigger('swipe', {
|
||||
direction,
|
||||
distance,
|
||||
angle,
|
||||
deltaX,
|
||||
deltaY,
|
||||
startPos: this.touchStartPos,
|
||||
endPos: this.touchMovePos,
|
||||
velocity: distance / (Date.now() - this.touchStartTime),
|
||||
});
|
||||
|
||||
// 触发特定方向的滑动事件
|
||||
this.trigger(`swipe${direction}`, {
|
||||
distance,
|
||||
velocity: distance / (Date.now() - this.touchStartTime),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理捏合缩放
|
||||
* @param {TouchEvent} e - 触摸事件
|
||||
*/
|
||||
handlePinch(e) {
|
||||
const touch1 = e.touches[0];
|
||||
const touch2 = e.touches[1];
|
||||
|
||||
const currentDistance = this.getDistance(
|
||||
{ x: touch1.clientX, y: touch1.clientY },
|
||||
{ x: touch2.clientX, y: touch2.clientY },
|
||||
);
|
||||
|
||||
if (!this.initialPinchDistance) {
|
||||
this.initialPinchDistance = currentDistance;
|
||||
return;
|
||||
}
|
||||
|
||||
const scale = currentDistance / this.initialPinchDistance;
|
||||
|
||||
this.trigger('pinch', {
|
||||
scale,
|
||||
center: {
|
||||
x: (touch1.clientX + touch2.clientX) / 2,
|
||||
y: (touch1.clientY + touch2.clientY) / 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两点距离
|
||||
* @param {Object} point1 - 点1
|
||||
* @param {Object} point2 - 点2
|
||||
* @returns {number} 距离
|
||||
*/
|
||||
getDistance(point1, point2) {
|
||||
const deltaX = point2.x - point1.x;
|
||||
const deltaY = point2.y - point1.y;
|
||||
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标事件处理(用于桌面端测试)
|
||||
*/
|
||||
handleMouseDown(e) {
|
||||
this.isMouseDown = true;
|
||||
this.handleTouchStart({
|
||||
touches: [{ clientX: e.clientX, clientY: e.clientY }],
|
||||
target: e.target,
|
||||
preventDefault: () => e.preventDefault(),
|
||||
});
|
||||
}
|
||||
|
||||
handleMouseMove(e) {
|
||||
if (!this.isMouseDown) return;
|
||||
this.handleTouchMove({
|
||||
touches: [{ clientX: e.clientX, clientY: e.clientY }],
|
||||
target: e.target,
|
||||
preventDefault: () => e.preventDefault(),
|
||||
});
|
||||
}
|
||||
|
||||
handleMouseUp(e) {
|
||||
if (!this.isMouseDown) return;
|
||||
this.isMouseDown = false;
|
||||
this.handleTouchEnd({
|
||||
target: e.target,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁手势监听
|
||||
*/
|
||||
destroy() {
|
||||
if (!this.element) return;
|
||||
|
||||
// 移除事件监听器
|
||||
this.element.removeEventListener('touchstart', this.handleTouchStart);
|
||||
this.element.removeEventListener('touchmove', this.handleTouchMove);
|
||||
this.element.removeEventListener('touchend', this.handleTouchEnd);
|
||||
this.element.removeEventListener('touchcancel', this.handleTouchCancel);
|
||||
this.element.removeEventListener('mousedown', this.handleMouseDown);
|
||||
this.element.removeEventListener('mousemove', this.handleMouseMove);
|
||||
this.element.removeEventListener('mouseup', this.handleMouseUp);
|
||||
|
||||
// 清除定时器
|
||||
if (this.longPressTimer) {
|
||||
clearTimeout(this.longPressTimer);
|
||||
}
|
||||
|
||||
// 清除回调
|
||||
this.gestureCallbacks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面滑动切换功能
|
||||
*/
|
||||
class PageSwipeGesture {
|
||||
constructor() {
|
||||
this.currentPageIndex = 0;
|
||||
this.pages = ['home', 'workbench', 'profile'];
|
||||
this.isAnimating = false;
|
||||
}
|
||||
|
||||
init() {
|
||||
const gesture = new GestureUtils();
|
||||
gesture.init(document.body, {
|
||||
enableSwipe: true,
|
||||
enableLongPress: false,
|
||||
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]);
|
||||
}
|
||||
|
||||
prevPage() {
|
||||
if (this.isAnimating) return;
|
||||
|
||||
this.currentPageIndex = (this.currentPageIndex - 1 + this.pages.length) % this.pages.length;
|
||||
this.switchPage(this.pages[this.currentPageIndex]);
|
||||
}
|
||||
|
||||
switchPage(pageName) {
|
||||
this.isAnimating = true;
|
||||
|
||||
const pageUrls = {
|
||||
home: '/pages/index/index',
|
||||
workbench: '/pages/workbench/index',
|
||||
profile: '/pages/profile/index',
|
||||
};
|
||||
|
||||
if (pageUrls[pageName]) {
|
||||
uni.switchTab({
|
||||
url: pageUrls[pageName],
|
||||
success: () => {
|
||||
setTimeout(() => {
|
||||
this.isAnimating = false;
|
||||
}, 300);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 长按菜单功能
|
||||
*/
|
||||
class LongPressMenu {
|
||||
constructor() {
|
||||
this.menu = null;
|
||||
this.isShowing = false;
|
||||
}
|
||||
|
||||
show(position, items = []) {
|
||||
if (this.isShowing) return;
|
||||
|
||||
this.isShowing = true;
|
||||
|
||||
// 创建菜单HTML
|
||||
const menuHtml = `
|
||||
<div class="longpress-menu" style="
|
||||
position: fixed;
|
||||
left: ${position.x}px;
|
||||
top: ${position.y}px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
z-index: 10000;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
">
|
||||
${items
|
||||
.map(
|
||||
(item) => `
|
||||
<div class="menu-item" style="
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
" data-action="${item.action}">
|
||||
${item.icon} ${item.text}
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', menuHtml);
|
||||
this.menu = document.querySelector('.longpress-menu');
|
||||
|
||||
// 绑定点击事件
|
||||
this.menu.addEventListener('click', (e) => {
|
||||
const action = e.target.dataset.action;
|
||||
if (action && items.find((item) => item.action === action)) {
|
||||
const item = items.find((item) => item.action === action);
|
||||
if (item.callback) item.callback();
|
||||
}
|
||||
this.hide();
|
||||
});
|
||||
|
||||
// 点击其他地方隐藏菜单
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.hideOnClickOutside.bind(this));
|
||||
}, 100);
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.isShowing || !this.menu) return;
|
||||
|
||||
this.menu.style.animation = 'fadeOut 0.2s ease-in forwards';
|
||||
setTimeout(() => {
|
||||
if (this.menu && this.menu.parentNode) {
|
||||
this.menu.remove();
|
||||
}
|
||||
this.isShowing = false;
|
||||
}, 200);
|
||||
|
||||
document.removeEventListener('click', this.hideOnClickOutside);
|
||||
}
|
||||
|
||||
hideOnClickOutside(e) {
|
||||
if (this.menu && !this.menu.contains(e.target)) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出工具类
|
||||
export { GestureUtils, PageSwipeGesture, LongPressMenu };
|
||||
export default GestureUtils;
|
||||
@@ -1,226 +1,120 @@
|
||||
/**
|
||||
* 网络请求工具类
|
||||
*/
|
||||
// API请求配置
|
||||
const baseURL =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? 'http://localhost:3000/api'
|
||||
: 'https://your-api-domain.com/api'
|
||||
|
||||
import { getToken, logout } from './auth.js';
|
||||
|
||||
// 基础URL配置 - 修正为本地后端地址
|
||||
const BASE_URL = 'http://localhost:8080';
|
||||
|
||||
// 请求超时时间
|
||||
const TIMEOUT = 10000;
|
||||
|
||||
/**
|
||||
* 网络请求封装
|
||||
*/
|
||||
export function request(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = getToken();
|
||||
|
||||
// 默认配置
|
||||
const defaultOptions = {
|
||||
url: BASE_URL + options.url,
|
||||
method: options.method || 'GET',
|
||||
timeout: TIMEOUT,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.header,
|
||||
},
|
||||
data: options.data || {},
|
||||
};
|
||||
|
||||
// 添加Authorization header
|
||||
if (token) {
|
||||
defaultOptions.header['Authorization'] = `Bearer ${token}`;
|
||||
// 统一请求方法
|
||||
export const request = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: baseURL + options.url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: getToken(),
|
||||
...options.header,
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
|
||||
// 显示加载提示
|
||||
if (options.loading !== false) {
|
||||
uni.showLoading({
|
||||
title: options.loadingText || '加载中...',
|
||||
mask: true,
|
||||
});
|
||||
}
|
||||
|
||||
uni.request({
|
||||
...defaultOptions,
|
||||
success: (res) => {
|
||||
// 隐藏加载提示
|
||||
if (options.loading !== false) {
|
||||
uni.hideLoading();
|
||||
}
|
||||
|
||||
const { statusCode, data } = res;
|
||||
|
||||
// 请求成功
|
||||
if (statusCode === 200) {
|
||||
// 业务成功
|
||||
if (data.code === 200 || data.success === true) {
|
||||
resolve(data);
|
||||
} else {
|
||||
// 业务失败
|
||||
const message = data.message || data.msg || '请求失败';
|
||||
|
||||
// Token过期,跳转登录
|
||||
if (data.code === 401) {
|
||||
uni.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none',
|
||||
});
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示错误信息
|
||||
if (options.showError !== false) {
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
|
||||
reject(new Error(message));
|
||||
}
|
||||
} else {
|
||||
// HTTP状态码错误
|
||||
const message = `请求失败 (${statusCode})`;
|
||||
|
||||
if (options.showError !== false) {
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
|
||||
reject(new Error(message));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
// 隐藏加载提示
|
||||
if (options.loading !== false) {
|
||||
uni.hideLoading();
|
||||
}
|
||||
|
||||
const message = err.errMsg || '网络请求失败';
|
||||
|
||||
if (options.showError !== false) {
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
|
||||
reject(new Error(message));
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
fail: (error) => {
|
||||
reject(error)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* GET请求
|
||||
*/
|
||||
export function get(url, params = {}, options = {}) {
|
||||
return request({
|
||||
url,
|
||||
method: 'GET',
|
||||
data: params,
|
||||
...options,
|
||||
});
|
||||
// 获取token
|
||||
function getToken() {
|
||||
try {
|
||||
const token = uni.getStorageSync('token')
|
||||
return token ? `Bearer ${token}` : ''
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST请求
|
||||
*/
|
||||
export function post(url, data = {}, options = {}) {
|
||||
return request({
|
||||
url,
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
});
|
||||
// API方法封装
|
||||
export const api = {
|
||||
// 用户登录
|
||||
login: (data) =>
|
||||
request({
|
||||
url: '/auth/login',
|
||||
method: 'POST',
|
||||
data,
|
||||
}),
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo: () =>
|
||||
request({
|
||||
url: '/user/info',
|
||||
}),
|
||||
|
||||
// 获取首页数据
|
||||
getHomeData: () =>
|
||||
request({
|
||||
url: '/home/data',
|
||||
}),
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT请求
|
||||
*/
|
||||
export function put(url, data = {}, options = {}) {
|
||||
return request({
|
||||
url,
|
||||
method: 'PUT',
|
||||
data,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
// 数据缓存工具
|
||||
export const storage = {
|
||||
// 设置缓存
|
||||
set: (key, value, expire) => {
|
||||
try {
|
||||
const data = {
|
||||
value,
|
||||
timestamp: Date.now(),
|
||||
expire: expire ? Date.now() + expire * 1000 : null,
|
||||
}
|
||||
uni.setStorageSync(key, JSON.stringify(data))
|
||||
} catch (e) {
|
||||
console.error('缓存设置失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* DELETE请求
|
||||
*/
|
||||
export function del(url, params = {}, options = {}) {
|
||||
return request({
|
||||
url,
|
||||
method: 'DELETE',
|
||||
data: params,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
// 获取缓存
|
||||
get: (key) => {
|
||||
try {
|
||||
const data = uni.getStorageSync(key)
|
||||
if (!data) return null
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
export function uploadFile(url, filePath, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = getToken();
|
||||
const parsed = JSON.parse(data)
|
||||
|
||||
// 显示上传进度
|
||||
if (options.loading !== false) {
|
||||
uni.showLoading({
|
||||
title: options.loadingText || '上传中...',
|
||||
mask: true,
|
||||
});
|
||||
}
|
||||
// 检查是否过期
|
||||
if (parsed.expire && Date.now() > parsed.expire) {
|
||||
uni.removeStorageSync(key)
|
||||
return null
|
||||
}
|
||||
|
||||
uni.uploadFile({
|
||||
url: BASE_URL + url,
|
||||
filePath,
|
||||
name: options.name || 'file',
|
||||
header: {
|
||||
Authorization: token ? `Bearer ${token}` : '',
|
||||
...options.header,
|
||||
},
|
||||
formData: options.formData || {},
|
||||
success: (res) => {
|
||||
if (options.loading !== false) {
|
||||
uni.hideLoading();
|
||||
}
|
||||
return parsed.value
|
||||
} catch (e) {
|
||||
console.error('缓存获取失败:', e)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
try {
|
||||
const data = JSON.parse(res.data);
|
||||
if (data.code === 200 || data.success === true) {
|
||||
resolve(data);
|
||||
} else {
|
||||
const message = data.message || data.msg || '上传失败';
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
});
|
||||
reject(new Error(message));
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error('响应数据解析失败'));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
if (options.loading !== false) {
|
||||
uni.hideLoading();
|
||||
}
|
||||
// 删除缓存
|
||||
remove: (key) => {
|
||||
try {
|
||||
uni.removeStorageSync(key)
|
||||
} catch (e) {
|
||||
console.error('缓存删除失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
const message = err.errMsg || '上传失败';
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
});
|
||||
reject(new Error(message));
|
||||
},
|
||||
});
|
||||
});
|
||||
// 清空缓存
|
||||
clear: () => {
|
||||
try {
|
||||
uni.clearStorageSync()
|
||||
} catch (e) {
|
||||
console.error('缓存清空失败:', e)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// SSE (Server-Sent Events) 实时数据工具
|
||||
class SSEClient {
|
||||
constructor() {
|
||||
this.eventSource = null
|
||||
this.listeners = {}
|
||||
this.isConnected = false
|
||||
}
|
||||
|
||||
// 连接SSE
|
||||
connect(url, options = {}) {
|
||||
if (this.eventSource) {
|
||||
this.disconnect()
|
||||
}
|
||||
|
||||
try {
|
||||
// 在H5环境下使用原生 EventSource
|
||||
if (typeof EventSource !== 'undefined') {
|
||||
this.eventSource = new EventSource(url)
|
||||
|
||||
this.eventSource.onopen = () => {
|
||||
this.isConnected = true
|
||||
console.log('SSE连接已建立')
|
||||
this.emit('connected')
|
||||
}
|
||||
|
||||
this.eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
this.emit('message', data)
|
||||
} catch (e) {
|
||||
this.emit('message', event.data)
|
||||
}
|
||||
}
|
||||
|
||||
this.eventSource.onerror = (error) => {
|
||||
this.isConnected = false
|
||||
console.error('SSE连接错误:', error)
|
||||
this.emit('error', error)
|
||||
|
||||
// 自动重连
|
||||
if (options.autoReconnect !== false) {
|
||||
setTimeout(() => {
|
||||
this.connect(url, options)
|
||||
}, options.reconnectDelay || 5000)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 在非H5环境下使用轮询模拟
|
||||
this.startPolling(url, options)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SSE连接失败:', error)
|
||||
this.emit('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 轮询模拟SSE(用于小程序等不支持SSE的环境)
|
||||
startPolling(url, options = {}) {
|
||||
const interval = options.pollingInterval || 10000 // 默认10秒
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const response = await uni.request({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
header: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
this.emit('message', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('轮询请求失败:', error)
|
||||
this.emit('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
this.pollingTimer = setInterval(poll, interval)
|
||||
this.isConnected = true
|
||||
this.emit('connected')
|
||||
|
||||
// 立即执行一次
|
||||
poll()
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
disconnect() {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close()
|
||||
this.eventSource = null
|
||||
}
|
||||
|
||||
if (this.pollingTimer) {
|
||||
clearInterval(this.pollingTimer)
|
||||
this.pollingTimer = null
|
||||
}
|
||||
|
||||
this.isConnected = false
|
||||
this.emit('disconnected')
|
||||
}
|
||||
|
||||
// 添加事件监听
|
||||
on(event, callback) {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = []
|
||||
}
|
||||
this.listeners[event].push(callback)
|
||||
}
|
||||
|
||||
// 移除事件监听
|
||||
off(event, callback) {
|
||||
if (this.listeners[event]) {
|
||||
const index = this.listeners[event].indexOf(callback)
|
||||
if (index > -1) {
|
||||
this.listeners[event].splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 触发事件
|
||||
emit(event, data) {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event].forEach((callback) => {
|
||||
try {
|
||||
callback(data)
|
||||
} catch (error) {
|
||||
console.error('事件回调执行错误:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取连接状态
|
||||
getConnectionState() {
|
||||
return this.isConnected
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局SSE实例
|
||||
export const sseClient = new SSEClient()
|
||||
|
||||
// 消息推送管理
|
||||
export const messageManager = {
|
||||
// 订阅消息
|
||||
subscribe(type, callback) {
|
||||
sseClient.on('message', (data) => {
|
||||
if (data.type === type) {
|
||||
callback(data.payload)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 取消订阅
|
||||
unsubscribe(type, callback) {
|
||||
sseClient.off('message', callback)
|
||||
},
|
||||
|
||||
// 连接实时消息服务
|
||||
connect(userId) {
|
||||
const sseUrl = `${
|
||||
process.env.NODE_ENV === 'development'
|
||||
? 'http://localhost:3000'
|
||||
: 'https://your-api-domain.com'
|
||||
}/sse/messages?userId=${userId}`
|
||||
|
||||
sseClient.connect(sseUrl, {
|
||||
autoReconnect: true,
|
||||
reconnectDelay: 5000,
|
||||
pollingInterval: 10000,
|
||||
})
|
||||
},
|
||||
|
||||
// 断开连接
|
||||
disconnect() {
|
||||
sseClient.disconnect()
|
||||
},
|
||||
}
|
||||