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提供准确的移动端开发上下文,确保生成代码完全符合实际项目架构。
This commit is contained in:
zlwu committed 2025-07-04 15:51:33 +08:00
1 parent e8ece1b612
commit 28232d5d46
131 files changed
+17170 -5479

No files matched your search

+776
View File
@@ -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 };
+422
View File
@@ -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 };
-127
View File
@@ -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;
}
+204
View File
@@ -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
},
}
+522
View File
@@ -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;
+105 -211
View File
@@ -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)
}
},
}
+179
View File
@@ -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()
},
}