commit 1d71a02738b2a74c210033c103d6a7b0689b0e9b Author: Rajuahamedkst Date: Fri Sep 12 16:08:17 2025 +0800 Initial Commit diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..115cc02 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,31 @@ +/* + * Eslint config file + * Documentation: https://eslint.org/docs/user-guide/configuring/ + * Install the Eslint extension before using this feature. + */ +module.exports = { + env: { + es6: true, + browser: true, + node: true, + }, + ecmaFeatures: { + modules: true, + }, + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + }, + globals: { + wx: true, + App: true, + Page: true, + getCurrentPages: true, + getApp: true, + Component: true, + requirePlugin: true, + requireMiniProgram: true, + }, + // extends: 'eslint:recommended', + rules: {}, +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..14ea590 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Windows +[Dd]esktop.ini +Thumbs.db +$RECYCLE.BIN/ + +# macOS +.DS_Store +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes + +# Node.js +node_modules/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2b7bfcb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "cSpell.enabled": true, + "cSpell.ignorePaths": [ + "**/*.wxml", + "**/*.wxss" + ], + "git.ignoreLimitWarning": true +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/app.js b/app.js new file mode 100644 index 0000000..90bae59 --- /dev/null +++ b/app.js @@ -0,0 +1,507 @@ +// app.js +const config = require('./config/config.js'); +const authManager = require('./utils/auth.js'); +const apiClient = require('./utils/api-client.js'); +const accountSyncManager = require('./utils/account-sync.js'); +const animationManager = require('./utils/animation-manager.js'); + +App({ + globalData: { + userInfo: null, + isLoggedIn: false, + appConfig: { + name: config.appName, + version: config.appVersion + }, + systemInfo: null, + networkStatus: 'unknown' + }, + + // 小程序启动 + onLaunch: async function () { + console.log('=== FindMe小程序启动 ==='); + console.log('版本:', config.appVersion); + console.log('环境:', config.debug?.enabled ? '开发' : '生产'); + + try { + // 1. 初始化系统信息 + await this.initSystemInfo(); + + // 2. 监听网络状态 + this.initNetworkMonitoring(); + + // 3. 初始化认证管理器 + await this.initAuth(); + + // 4. 静默登录检查 + await this.checkSilentLogin(); + + console.log('=== 小程序启动完成 ==='); + + } catch (error) { + console.error('小程序启动过程中发生错误:', error); + // 即使出错也要继续启动,不能阻塞用户使用 + } + }, + + // 小程序显示 + onShow: function () { + console.log('小程序显示'); + + // 检查网络状态 + this.checkNetworkStatus(); + + // 如果用户已登录,检查token是否需要刷新 + if (this.globalData.isLoggedIn) { + this.refreshTokenIfNeeded(); + + // 🔥 自动检查账号同步状态(延迟执行,避免阻塞主流程) + setTimeout(() => { + this.checkAccountSync(); + }, 2000); + } + }, + + // 小程序隐藏 + onHide: function () { + console.log('小程序隐藏'); + }, + + // 小程序错误处理 + onError: function (error) { + console.error('小程序全局错误:', error); + + // 这里可以集成错误上报服务 + // 例如:errorReporter.report(error); + }, + + // 页面未找到 + onPageNotFound: function (res) { + console.error('页面未找到:', res); + + // 重定向到首页 + wx.reLaunch({ + url: '/pages/map/map' + }); + }, + + // 未处理的Promise拒绝 + onUnhandledRejection: function (res) { + console.error('未处理的Promise拒绝:', res); + }, + + // 初始化系统信息 + async initSystemInfo() { + try { + // 使用新的API替代废弃的wx.getSystemInfoSync + const [windowInfo, deviceInfo, appBaseInfo] = await Promise.all([ + this.getWindowInfo(), + this.getDeviceInfo(), + this.getAppBaseInfo() + ]); + + // 合并系统信息 + const systemInfo = { + ...windowInfo, + ...deviceInfo, + ...appBaseInfo + }; + + this.globalData.systemInfo = systemInfo; + + console.log('系统信息初始化成功:', { + platform: systemInfo.platform, + version: systemInfo.version, + model: systemInfo.model, + language: systemInfo.language + }); + + } catch (error) { + console.error('获取系统信息失败,使用兜底方案:', error); + // 兜底使用统一的系统信息工具 + try { + const { getSystemInfoSync } = require('./utils/system-info-helper.js'); + const systemInfo = getSystemInfoSync(); + this.globalData.systemInfo = systemInfo; + } catch (fallbackError) { + console.error('兜底方案也失败:', fallbackError); + } + } + }, + + // 获取窗口信息 + getWindowInfo() { + return new Promise((resolve) => { + try { + const windowInfo = wx.getWindowInfo(); + resolve(windowInfo); + } catch (error) { + console.warn('获取窗口信息失败:', error); + resolve({}); + } + }); + }, + + // 获取设备信息 + getDeviceInfo() { + return new Promise((resolve) => { + try { + const deviceInfo = wx.getDeviceInfo(); + resolve(deviceInfo); + } catch (error) { + console.warn('获取设备信息失败:', error); + resolve({}); + } + }); + }, + + // 获取应用基础信息 + getAppBaseInfo() { + return new Promise((resolve) => { + try { + const appBaseInfo = wx.getAppBaseInfo(); + resolve(appBaseInfo); + } catch (error) { + console.warn('获取应用信息失败:', error); + resolve({}); + } + }); + }, + + // 初始化网络监听 + initNetworkMonitoring() { + // 获取当前网络状态 + wx.getNetworkType({ + success: (res) => { + this.globalData.networkStatus = res.networkType; + console.log('当前网络类型:', res.networkType); + } + }); + + // 监听网络状态变化 + wx.onNetworkStatusChange((res) => { + this.globalData.networkStatus = res.networkType; + console.log('网络状态变化:', res); + + if (res.isConnected) { + console.log('网络连接恢复'); + // 网络恢复时,重新检查登录状态 + if (this.globalData.isLoggedIn) { + this.refreshTokenIfNeeded(); + } + } else { + console.log('网络连接断开'); + } + }); + }, + + // 初始化认证管理器 + async initAuth() { + try { + console.log('初始化认证管理器...'); + await authManager.init(); + console.log('认证管理器初始化完成'); + } catch (error) { + console.error('认证管理器初始化失败:', error); + } + }, + + // 静默登录检查 + async checkSilentLogin() { + try { + console.log('🔍 开始静默登录检查...'); + + // 🔥 先检查本地存储的token + const storedUserInfo = wx.getStorageSync('userInfo'); + const directToken = wx.getStorageSync('token'); + + console.log('🔍 本地token检查:', { + hasStoredUserInfo: !!storedUserInfo, + hasUserInfoToken: !!(storedUserInfo?.token), + hasDirectToken: !!directToken, + userInfoTokenLength: storedUserInfo?.token?.length || 0, + directTokenLength: directToken?.length || 0 + }); + + // 如果没有任何token,直接返回false + if (!storedUserInfo?.token && !directToken) { + console.log('❌ 没有找到任何token,用户需要登录'); + this.globalData.isLoggedIn = false; + this.globalData.userInfo = null; + return; + } + + const isAuthenticated = await authManager.silentLogin(); + + if (isAuthenticated) { + console.log('✅ 静默登录成功,用户已登录'); + this.globalData.isLoggedIn = true; + + // 🔥 再次验证token的有效性 + const finalUserInfo = wx.getStorageSync('userInfo'); + if (finalUserInfo && finalUserInfo.token && finalUserInfo.token.length > 0) { + this.globalData.userInfo = finalUserInfo; + console.log('✅ 已恢复完整用户信息,token长度:', finalUserInfo.token.length); + } else { + console.error('❌ 静默登录成功但无法获取有效token'); + this.globalData.isLoggedIn = false; + this.globalData.userInfo = null; + } + } else { + console.log('❌ 静默登录失败,用户需要手动登录'); + this.globalData.isLoggedIn = false; + this.globalData.userInfo = null; + + // 🔥 清理无效的存储数据 + wx.removeStorageSync('userInfo'); + wx.removeStorageSync('token'); + } + + } catch (error) { + console.error('❌ 静默登录检查失败:', error); + this.globalData.isLoggedIn = false; + this.globalData.userInfo = null; + + // 🔥 出错时清理存储数据 + wx.removeStorageSync('userInfo'); + wx.removeStorageSync('token'); + } + }, + + // 检查网络状态 + checkNetworkStatus() { + wx.getNetworkType({ + success: (res) => { + const oldStatus = this.globalData.networkStatus; + this.globalData.networkStatus = res.networkType; + + if (oldStatus !== res.networkType) { + console.log('网络类型变化:', oldStatus, '->', res.networkType); + } + }, + fail: (error) => { + console.error('获取网络类型失败:', error); + } + }); + }, + + // 刷新token(如果需要) + async refreshTokenIfNeeded() { + try { + if (!this.globalData.userInfo) { + return; + } + + const isValid = authManager.isTokenValid(this.globalData.userInfo); + if (!isValid) { + console.log('Token即将过期,尝试刷新'); + const refreshed = await authManager.refreshTokenIfNeeded(this.globalData.userInfo); + + if (refreshed) { + console.log('Token刷新成功'); + // 🔥 修复:获取完整的认证信息 + const storedUserInfo = wx.getStorageSync('userInfo'); + if (storedUserInfo && storedUserInfo.token) { + this.globalData.userInfo = storedUserInfo; + console.log('Token刷新后已更新完整用户信息'); + } + } else { + console.log('Token刷新失败,需要重新登录'); + this.handleAuthExpired(); + } + } + } catch (error) { + console.error('Token刷新检查失败:', error); + } + }, + + // 处理认证过期 + handleAuthExpired() { + console.log('认证已过期,清除登录状态'); + + authManager.clearAuthData(); + this.globalData.isLoggedIn = false; + this.globalData.userInfo = null; + + // 显示提示 + wx.showToast({ + title: '登录已过期,请重新登录', + icon: 'none', + duration: 2000 + }); + + // 跳转到登录页 + setTimeout(() => { + wx.reLaunch({ + url: '/pages/login/login' + }); + }, 2000); + }, + + // 手动登录方法(供页面调用) + async login(loginData) { + try { + console.log('执行登录,保存用户信息'); + + // 使用认证管理器保存登录信息 + const success = await authManager.saveAuthData(loginData); + + if (success) { + // 🔥 修复:获取完整的认证信息 + const storedUserInfo = wx.getStorageSync('userInfo'); + if (storedUserInfo && storedUserInfo.token) { + this.globalData.userInfo = storedUserInfo; + this.globalData.isLoggedIn = true; + console.log('登录信息保存成功,包含token'); + return true; + } else { + console.error('登录信息保存失败:无法获取完整用户信息'); + return false; + } + } else { + console.error('登录信息保存失败'); + return false; + } + + } catch (error) { + console.error('登录处理失败:', error); + return false; + } + }, + + // 登出方法(供页面调用) + async logout() { + try { + console.log('执行登出'); + + const success = await authManager.logout(); + + // 无论服务端登出是否成功,都要清除本地状态 + this.globalData.isLoggedIn = false; + this.globalData.userInfo = null; + + console.log('登出完成'); + return success; + + } catch (error) { + console.error('登出失败:', error); + return false; + } + }, + + // 检查是否需要登录(供页面调用) + requireAuth() { + return authManager.requireAuth(); + }, + + // 获取用户显示信息(供页面调用) + getUserDisplayInfo() { + return authManager.getUserDisplayInfo(); + }, + + // 检查用户权限(供页面调用) + hasPermission(permission) { + return authManager.hasPermission(permission); + }, + + // 获取当前token(供页面调用) + getCurrentToken() { + return authManager.getCurrentToken(); + }, + + // 等待认证初始化完成(供页面调用) + async waitForAuth() { + await authManager.waitForInitialization(); + return this.globalData.isLoggedIn; + }, + + // 检查网络连接 + isNetworkAvailable() { + return this.globalData.networkStatus !== 'none'; + }, + + // 显示网络错误提示 + showNetworkError() { + if (!this.isNetworkAvailable()) { + wx.showToast({ + title: '网络连接不可用', + icon: 'none', + duration: 2000 + }); + return true; + } + return false; + }, + + // 全局错误处理工具 + handleError(error, context = '') { + console.error(`${context} 错误:`, error); + + // 根据错误类型显示不同提示 + let message = '操作失败,请重试'; + + if (error.message) { + message = error.message; + } else if (error.errMsg) { + if (error.errMsg.includes('timeout')) { + message = '请求超时,请检查网络连接'; + } else if (error.errMsg.includes('fail')) { + message = '网络连接失败'; + } + } + + wx.showToast({ + title: message, + icon: 'none', + duration: 2000 + }); + }, + + // 显示加载状态 + showLoading(title = '加载中...') { + wx.showLoading({ + title: title, + mask: true + }); + }, + + // 隐藏加载状态 + hideLoading() { + wx.hideLoading(); + }, + + // 兼容性方法 - 检查登录状态 + isLoggedIn() { + return this.globalData.isLoggedIn; + }, + + // 兼容性方法 - 获取用户信息 + getUserInfo() { + return this.globalData.userInfo; + }, + + // 兼容性方法 - 获取token + getToken() { + return authManager.getCurrentToken(); + }, + + // 🔥 账号同步检查 + async checkAccountSync() { + try { + console.log('开始检查账号同步状态...'); + + // 检查是否应该跳过绑定提示 + if (accountSyncManager.shouldSkipBinding()) { + console.log('用户选择跳过绑定,24小时内不再提示'); + return; + } + + // 自动检查并引导用户绑定手机号 + await accountSyncManager.autoCheckAndBind(); + + } catch (error) { + console.error('账号同步检查失败:', error); + // 不影响主要功能,静默处理错误 + } + } +}) diff --git a/app.json b/app.json new file mode 100644 index 0000000..ac73141 --- /dev/null +++ b/app.json @@ -0,0 +1,60 @@ +{ + "pages": [ + "pages/map/map", + "pages/login/login", + "pages/message/message", + "pages/message/chat/chat", + "pages/social/friends/friends", + "pages/social/friend-detail/friend-detail", + "pages/social/search/search", + "pages/social/friend-requests/friend-requests", + "pages/profile/profile", + "pages/qr-code/qr-code", + "pages/edit/edit", + "pages/splash/splash", + "pages/account-sync/phone-binding/phone-binding", + "pages/websocket-test/websocket-test", + "pages/search/global-search", + "pages/group/create-group/create-group", + "pages/group/group-info/group-info", + "pages/group/group-members/group-members", + "pages/group/group-announcement/group-announcement", + "pages/settings/notification-settings/notification-settings", + "pages/personal-details/personal-details", + "pages/settings/about/update-log/update-log", + "pages/settingss/settingss", + "pages/settings/about/about", + "pages/settings/feedback/feedback", + "pages/settings/account-security/account-security" + ], + "window": { + "backgroundTextStyle": "light", + "navigationBarBackgroundColor": "#000000", + "navigationBarTitleText": "FindMe", + "navigationBarTextStyle": "white", + "backgroundColor": "#f8f9fa", + "enablePullDownRefresh": false, + "onReachBottomDistance": 50 + }, + + "networkTimeout": { + "request": 15000, + "downloadFile": 15000, + "uploadFile": 20000, + "connectSocket": 20000 + }, + "debug": false, + "permission": { + "scope.userLocation": { + "desc": "你的位置信息将用于小程序位置接口的效果展示" + } + }, + "requiredBackgroundModes": ["location"], + "requiredPrivateInfos": [ + "getLocation", + "chooseLocation" + ], + "style": "v2", + "sitemapLocation": "sitemap.json", + "lazyCodeLoading": "requiredComponents" +} \ No newline at end of file diff --git a/app.wxss b/app.wxss new file mode 100644 index 0000000..b245c71 --- /dev/null +++ b/app.wxss @@ -0,0 +1,55 @@ +/**app.wxss**/ +/* 导入设计系统和组件样式 */ +@import './styles/design-system.wxss'; +@import './styles/components.wxss'; +@import './styles/responsive.wxss'; +/* 导入通用屏幕适配样式 */ +@import './styles/screen-adaption.wxss'; + +/* 全局基础样式 */ +page { + height: 100%; + overflow: hidden; + margin: 0; + padding: 0; + box-sizing: border-box; + background-color: #f8f9fa; +} + +/* 默认容器样式 - 保持向后兼容 */ +.container { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 200rpx 0; + box-sizing: border-box; +} + +/* 全屏容器 - 新的推荐方式 */ +.app-container { + height: 100vh; + min-height: 100vh; + max-height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; + box-sizing: border-box; + position: relative; +} + +/* 内容区域 */ +.content-area { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; +} + +/* 防止长按选择 */ +text, view, image { + -webkit-user-select: none; + user-select: none; + box-sizing: border-box; +} diff --git a/components/interactive-feedback/interactive-feedback.js b/components/interactive-feedback/interactive-feedback.js new file mode 100644 index 0000000..93bde53 --- /dev/null +++ b/components/interactive-feedback/interactive-feedback.js @@ -0,0 +1,567 @@ +// 🎯 交互反馈组件逻辑 +const animationManager = require('../../utils/animation-manager.js'); + +Component({ + properties: { + // 反馈类型 + feedbackType: { + type: String, + value: 'button' // button, card, list-item, icon, floating + }, + + // 是否启用波纹效果 + ripple: { + type: Boolean, + value: true + }, + + // 波纹颜色 + rippleColor: { + type: String, + value: 'primary' // primary, white, dark + }, + + // 触摸反馈类型 + touchFeedback: { + type: String, + value: 'overlay' // overlay, highlight, glow, none + }, + + // 是否启用缩放效果 + scaleEffect: { + type: Boolean, + value: true + }, + + // 缩放类型 + scaleType: { + type: String, + value: 'normal' // normal, large, none + }, + + // 是否禁用 + disabled: { + type: Boolean, + value: false + }, + + // 加载状态 + loading: { + type: Boolean, + value: false, + observer: 'onLoadingChange' + }, + + // 主题 + theme: { + type: String, + value: 'default' // default, primary, success, error, ghost, minimal + }, + + // 特殊效果 + effect: { + type: String, + value: 'none' // none, glass, neon, gradient, hover-lift, hover-glow + } + }, + + data: { + // 反馈状态 + pressed: false, + active: false, + + // 波纹数据 + ripples: [], + showRipple: false, + + // 触摸反馈 + showTouchFeedback: false, + touchFeedbackClass: '', + touchFeedbackStyle: '', + + // 动画数据 + animationData: null, + loadingAnimation: null, + successAnimation: null, + errorAnimation: null, + + // 样式 + feedbackStyle: '', + feedbackClass: '', + + // 状态反馈 + showSuccess: false, + showError: false, + + // 定时器 + rippleTimer: null, + feedbackTimer: null + }, + + lifetimes: { + attached() { + console.log('🎯 交互反馈组件加载'); + this.initComponent(); + }, + + detached() { + console.log('🎯 交互反馈组件卸载'); + this.cleanup(); + } + }, + + methods: { + // 初始化组件 + initComponent() { + this.updateFeedbackClass(); + this.updateTouchFeedbackClass(); + this.createLoadingAnimation(); + }, + + // 加载状态变化处理 + onLoadingChange(loading) { + if (loading) { + this.startLoadingAnimation(); + } else { + this.stopLoadingAnimation(); + } + }, + + // 🎯 ===== 触摸事件处理 ===== + + // 触摸开始 + onTouchStart(e) { + if (this.properties.disabled || this.properties.loading) { + return; + } + + console.log('🎯 触摸开始'); + + this.setData({ + pressed: true + }); + + // 显示触摸反馈 + this.showTouchFeedbackEffect(); + + // 创建波纹效果 + if (this.properties.ripple) { + this.createRipple(e); + } + + // 缩放效果 + if (this.properties.scaleEffect) { + this.applyScaleEffect(true); + } + + this.triggerEvent('touchstart', e); + }, + + // 触摸结束 + onTouchEnd(e) { + if (this.properties.disabled || this.properties.loading) { + return; + } + + console.log('🎯 触摸结束'); + + this.setData({ + pressed: false + }); + + // 隐藏触摸反馈 + this.hideTouchFeedbackEffect(); + + // 恢复缩放 + if (this.properties.scaleEffect) { + this.applyScaleEffect(false); + } + + this.triggerEvent('touchend', e); + }, + + // 触摸取消 + onTouchCancel(e) { + if (this.properties.disabled || this.properties.loading) { + return; + } + + console.log('🎯 触摸取消'); + + this.setData({ + pressed: false + }); + + // 隐藏触摸反馈 + this.hideTouchFeedbackEffect(); + + // 恢复缩放 + if (this.properties.scaleEffect) { + this.applyScaleEffect(false); + } + + this.triggerEvent('touchcancel', e); + }, + + // 点击事件 + onTap(e) { + if (this.properties.disabled || this.properties.loading) { + return; + } + + console.log('🎯 点击事件'); + + // 按钮点击动画 + this.playButtonPressAnimation(); + + this.triggerEvent('tap', e); + }, + + // 长按事件 + onLongPress(e) { + if (this.properties.disabled || this.properties.loading) { + return; + } + + console.log('🎯 长按事件'); + + // 长按反馈 + this.playLongPressFeedback(); + + this.triggerEvent('longpress', e); + }, + + // 🌊 ===== 波纹效果 ===== + + // 创建波纹 + createRipple(e) { + const touch = e.touches[0]; + if (!touch) return; + + // 获取组件位置信息 + this.createSelectorQuery() + .select('.interactive-feedback-container') + .boundingClientRect((rect) => { + if (!rect) return; + + // 计算波纹位置 + const x = touch.clientX - rect.left; + const y = touch.clientY - rect.top; + + // 计算波纹大小 + const size = Math.max(rect.width, rect.height) * 2; + + // 创建波纹数据 + const ripple = { + id: this.generateRippleId(), + class: `${this.properties.rippleColor} animate`, + style: ` + left: ${x - size / 2}px; + top: ${y - size / 2}px; + width: ${size}px; + height: ${size}px; + `, + animation: null + }; + + // 添加波纹 + const ripples = [...this.data.ripples, ripple]; + this.setData({ + ripples: ripples, + showRipple: true + }); + + // 清理波纹 + this.rippleTimer = setTimeout(() => { + this.removeRipple(ripple.id); + }, 600); + + }) + .exec(); + }, + + // 移除波纹 + removeRipple(rippleId) { + const ripples = this.data.ripples.filter(ripple => ripple.id !== rippleId); + this.setData({ + ripples: ripples, + showRipple: ripples.length > 0 + }); + }, + + // 清理所有波纹 + clearRipples() { + this.setData({ + ripples: [], + showRipple: false + }); + }, + + // 📱 ===== 触摸反馈 ===== + + // 显示触摸反馈 + showTouchFeedbackEffect() { + if (this.properties.touchFeedback === 'none') return; + + this.setData({ + showTouchFeedback: true, + touchFeedbackClass: `${this.properties.touchFeedback} active` + }); + }, + + // 隐藏触摸反馈 + hideTouchFeedbackEffect() { + this.setData({ + showTouchFeedback: false, + touchFeedbackClass: this.properties.touchFeedback + }); + }, + + // 🎭 ===== 动画效果 ===== + + // 应用缩放效果 + applyScaleEffect(pressed) { + if (!this.properties.scaleEffect) return; + + let scale = 1; + if (pressed) { + switch (this.properties.scaleType) { + case 'large': + scale = 1.02; + break; + case 'none': + scale = 1; + break; + case 'normal': + default: + scale = 0.98; + break; + } + } + + const animation = animationManager.scale(scale, { + duration: 150, + timingFunction: 'ease-out' + }); + + this.setData({ + animationData: animation.export() + }); + }, + + // 播放按钮点击动画 + playButtonPressAnimation() { + const animation = animationManager.buttonPress({ + duration: 200 + }); + + this.setData({ + animationData: animation.export() + }); + }, + + // 播放长按反馈 + playLongPressFeedback() { + const animation = animationManager.pulse({ + duration: 300 + }); + + this.setData({ + animationData: animation.export() + }); + }, + + // 🔄 ===== 加载动画 ===== + + // 创建加载动画 + createLoadingAnimation() { + const loadingAnimation = animationManager.loadingSpinner({ + duration: 1000 + }); + + this.setData({ + loadingAnimation: loadingAnimation.export() + }); + }, + + // 开始加载动画 + startLoadingAnimation() { + this.loadingTimer = setInterval(() => { + const loadingAnimation = animationManager.loadingSpinner({ + duration: 1000 + }); + + this.setData({ + loadingAnimation: loadingAnimation.export() + }); + }, 1000); + }, + + // 停止加载动画 + stopLoadingAnimation() { + if (this.loadingTimer) { + clearInterval(this.loadingTimer); + this.loadingTimer = null; + } + }, + + // ✅ ===== 状态反馈 ===== + + // 显示成功反馈 + showSuccessFeedback(duration = 1500) { + console.log('✅ 显示成功反馈'); + + this.setData({ + showSuccess: true + }); + + const successAnimation = animationManager.bounceIn({ + duration: 500 + }); + + this.setData({ + successAnimation: successAnimation.export() + }); + + // 自动隐藏 + setTimeout(() => { + this.hideSuccessFeedback(); + }, duration); + }, + + // 隐藏成功反馈 + hideSuccessFeedback() { + this.setData({ + showSuccess: false + }); + }, + + // 显示错误反馈 + showErrorFeedback(duration = 1500) { + console.log('❌ 显示错误反馈'); + + this.setData({ + showError: true + }); + + const errorAnimation = animationManager.shake({ + duration: 500 + }); + + this.setData({ + errorAnimation: errorAnimation.export() + }); + + // 自动隐藏 + setTimeout(() => { + this.hideErrorFeedback(); + }, duration); + }, + + // 隐藏错误反馈 + hideErrorFeedback() { + this.setData({ + showError: false + }); + }, + + // 🎨 ===== 样式管理 ===== + + // 更新反馈类 + updateFeedbackClass() { + let feedbackClass = this.properties.feedbackType; + + if (this.properties.theme !== 'default') { + feedbackClass += ` theme-${this.properties.theme}`; + } + + if (this.properties.effect !== 'none') { + feedbackClass += ` ${this.properties.effect}`; + } + + if (this.properties.disabled) { + feedbackClass += ' disabled'; + } + + if (this.properties.loading) { + feedbackClass += ' loading'; + } + + if (this.data.pressed) { + feedbackClass += ' pressed'; + } + + if (this.data.active) { + feedbackClass += ' active'; + } + + this.setData({ + feedbackClass: feedbackClass + }); + }, + + // 更新触摸反馈类 + updateTouchFeedbackClass() { + this.setData({ + touchFeedbackClass: this.properties.touchFeedback + }); + }, + + // 🔧 ===== 工具方法 ===== + + // 生成波纹ID + generateRippleId() { + return `ripple_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + }, + + // 设置激活状态 + setActive(active) { + this.setData({ + active: active + }); + this.updateFeedbackClass(); + }, + + // 触发成功状态 + triggerSuccess() { + this.showSuccessFeedback(); + this.triggerEvent('success'); + }, + + // 触发错误状态 + triggerError() { + this.showErrorFeedback(); + this.triggerEvent('error'); + }, + + // 重置状态 + reset() { + this.setData({ + pressed: false, + active: false, + showSuccess: false, + showError: false + }); + + this.clearRipples(); + this.hideTouchFeedbackEffect(); + this.updateFeedbackClass(); + }, + + // 清理资源 + cleanup() { + if (this.rippleTimer) { + clearTimeout(this.rippleTimer); + this.rippleTimer = null; + } + + if (this.feedbackTimer) { + clearTimeout(this.feedbackTimer); + this.feedbackTimer = null; + } + + this.stopLoadingAnimation(); + this.clearRipples(); + } + } +}); diff --git a/components/interactive-feedback/interactive-feedback.json b/components/interactive-feedback/interactive-feedback.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/components/interactive-feedback/interactive-feedback.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/components/interactive-feedback/interactive-feedback.wxml b/components/interactive-feedback/interactive-feedback.wxml new file mode 100644 index 0000000..92ad739 --- /dev/null +++ b/components/interactive-feedback/interactive-feedback.wxml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/interactive-feedback/interactive-feedback.wxss b/components/interactive-feedback/interactive-feedback.wxss new file mode 100644 index 0000000..7b56786 --- /dev/null +++ b/components/interactive-feedback/interactive-feedback.wxss @@ -0,0 +1,446 @@ +/* 🎯 交互反馈组件样式 */ + +/* CSS变量定义 */ +.interactive-feedback-container { + --primary-color: #007AFF; + --primary-light: #5AC8FA; + --success-color: #34C759; + --error-color: #FF3B30; + --background-color: #F2F2F7; + --surface-color: #FFFFFF; + --text-primary: #000000; + --text-secondary: #8E8E93; + --border-color: #E5E5EA; + --shadow-light: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); + --radius-medium: 12rpx; +} + +/* 🌙 深色模式支持 */ +@media (prefers-color-scheme: dark) { + .interactive-feedback-container { + --primary-color: #0A84FF; + --background-color: #000000; + --surface-color: #1C1C1E; + --text-primary: #FFFFFF; + --text-secondary: #8E8E93; + --border-color: #38383A; + --shadow-light: 0 2rpx 8rpx rgba(0, 0, 0, 0.3); + } +} + +.interactive-feedback-container { + position: relative; + overflow: hidden; + transition: all 0.2s ease; + user-select: none; + -webkit-user-select: none; + -webkit-tap-highlight-color: transparent; +} + +/* 🎨 反馈类型样式 */ +.interactive-feedback-container.button { + border-radius: var(--radius-medium); + background: var(--surface-color); + border: 1rpx solid var(--border-color); + box-shadow: var(--shadow-light); +} + +.interactive-feedback-container.card { + border-radius: var(--radius-medium); + background: var(--surface-color); + box-shadow: var(--shadow-light); +} + +.interactive-feedback-container.list-item { + background: var(--surface-color); + border-bottom: 1rpx solid var(--border-color); +} + +.interactive-feedback-container.icon { + border-radius: 50%; + background: var(--background-color); +} + +.interactive-feedback-container.floating { + border-radius: 50%; + background: var(--primary-color); + box-shadow: 0 8rpx 24rpx rgba(0, 122, 255, 0.3); +} + +/* 🎭 状态样式 */ +.interactive-feedback-container.pressed { + transform: scale(0.98); +} + +.interactive-feedback-container.active { + background: var(--primary-color); + color: white; +} + +.interactive-feedback-container.disabled { + opacity: 0.5; + pointer-events: none; +} + +.interactive-feedback-container.loading { + pointer-events: none; +} + +.interactive-feedback-container.success { + background: var(--success-color); + color: white; +} + +.interactive-feedback-container.error { + background: var(--error-color); + color: white; +} + +/* 🌊 波纹效果 */ +.ripple-container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: hidden; + pointer-events: none; + border-radius: inherit; +} + +.ripple { + position: absolute; + border-radius: 50%; + pointer-events: none; + transform: scale(0); + opacity: 0.3; +} + +.ripple.primary { + background: var(--primary-color); +} + +.ripple.white { + background: rgba(255, 255, 255, 0.5); +} + +.ripple.dark { + background: rgba(0, 0, 0, 0.1); +} + +/* 📱 触摸反馈 */ +.touch-feedback { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + border-radius: inherit; + opacity: 0; + transition: opacity 0.2s ease; +} + +.touch-feedback.overlay { + background: rgba(0, 0, 0, 0.05); +} + +.touch-feedback.highlight { + background: rgba(0, 122, 255, 0.1); +} + +.touch-feedback.glow { + box-shadow: 0 0 20rpx rgba(0, 122, 255, 0.5); +} + +.touch-feedback.active { + opacity: 1; +} + +/* 🔄 加载状态 */ +.loading-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.8); + border-radius: inherit; +} + +.loading-spinner { + width: 40rpx; + height: 40rpx; + border: 3rpx solid rgba(0, 122, 255, 0.2); + border-top: 3rpx solid var(--primary-color); + border-radius: 50%; +} + +/* ✅ 成功反馈 */ +.success-feedback { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 80rpx; + height: 80rpx; + border-radius: 50%; + background: var(--success-color); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transform: translate(-50%, -50%) scale(0); +} + +.success-icon { + font-size: 40rpx; + color: white; + font-weight: bold; +} + +/* ❌ 错误反馈 */ +.error-feedback { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 80rpx; + height: 80rpx; + border-radius: 50%; + background: var(--error-color); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transform: translate(-50%, -50%) scale(0); +} + +.error-icon { + font-size: 40rpx; + color: white; + font-weight: bold; +} + +/* 🎪 动画效果 */ +@keyframes rippleExpand { + from { + transform: scale(0); + opacity: 0.3; + } + to { + transform: scale(4); + opacity: 0; + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@keyframes successPop { + 0% { + opacity: 0; + transform: translate(-50%, -50%) scale(0); + } + 50% { + opacity: 1; + transform: translate(-50%, -50%) scale(1.2); + } + 100% { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + +@keyframes errorShake { + 0%, 100% { + transform: translate(-50%, -50%) translateX(0); + } + 25% { + transform: translate(-50%, -50%) translateX(-10rpx); + } + 75% { + transform: translate(-50%, -50%) translateX(10rpx); + } +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } +} + +@keyframes bounce { + 0%, 20%, 53%, 80%, 100% { + transform: scale(1); + } + 40%, 43% { + transform: scale(1.1); + } + 70% { + transform: scale(1.05); + } + 90% { + transform: scale(1.02); + } +} + +/* 🎭 动画类应用 */ +.ripple.animate { + animation: rippleExpand 0.6s ease-out; +} + +.loading-spinner { + animation: spin 1s linear infinite; +} + +.success-feedback.animate { + animation: successPop 0.5s ease-out; +} + +.error-feedback.animate { + animation: errorShake 0.5s ease-out; +} + +.interactive-feedback-container.pulse { + animation: pulse 1s ease-in-out infinite; +} + +.interactive-feedback-container.bounce { + animation: bounce 1s ease-in-out; +} + +/* 🎨 特殊效果 */ +.interactive-feedback-container.glass { + backdrop-filter: blur(20rpx); + background: rgba(255, 255, 255, 0.1); + border: 1rpx solid rgba(255, 255, 255, 0.2); +} + +.interactive-feedback-container.neon { + box-shadow: + 0 0 10rpx var(--primary-color), + 0 0 20rpx var(--primary-color), + 0 0 40rpx var(--primary-color); +} + +.interactive-feedback-container.gradient { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%); +} + +/* 📱 响应式设计 */ +@media screen and (max-width: 375px) { + .success-feedback, + .error-feedback { + width: 60rpx; + height: 60rpx; + } + + .success-icon, + .error-icon { + font-size: 30rpx; + } + + .loading-spinner { + width: 30rpx; + height: 30rpx; + border-width: 2rpx; + } +} + +@media screen and (min-width: 414px) { + .success-feedback, + .error-feedback { + width: 100rpx; + height: 100rpx; + } + + .success-icon, + .error-icon { + font-size: 50rpx; + } + + .loading-spinner { + width: 50rpx; + height: 50rpx; + border-width: 4rpx; + } +} + +/* 🎯 交互状态 */ +.interactive-feedback-container:active { + transform: scale(0.98); +} + +.interactive-feedback-container.no-scale:active { + transform: none; +} + +.interactive-feedback-container.scale-large:active { + transform: scale(1.02); +} + +/* 🔧 性能优化 */ +.interactive-feedback-container { + will-change: transform, opacity; + backface-visibility: hidden; + -webkit-backface-visibility: hidden; +} + +.ripple { + will-change: transform, opacity; +} + +/* 🎪 组合效果 */ +.interactive-feedback-container.hover-lift { + transition: all 0.3s ease; +} + +.interactive-feedback-container.hover-lift:hover { + transform: translateY(-4rpx); + box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.15); +} + +.interactive-feedback-container.hover-glow:hover { + box-shadow: 0 0 30rpx rgba(0, 122, 255, 0.4); +} + +/* 🎭 主题变体 */ +.interactive-feedback-container.theme-primary { + background: var(--primary-color); + color: white; +} + +.interactive-feedback-container.theme-success { + background: var(--success-color); + color: white; +} + +.interactive-feedback-container.theme-error { + background: var(--error-color); + color: white; +} + +.interactive-feedback-container.theme-ghost { + background: transparent; + border: 2rpx solid var(--primary-color); + color: var(--primary-color); +} + +.interactive-feedback-container.theme-minimal { + background: transparent; + border: none; + box-shadow: none; +} diff --git a/components/media-preview/media-preview.js b/components/media-preview/media-preview.js new file mode 100644 index 0000000..72b0d13 --- /dev/null +++ b/components/media-preview/media-preview.js @@ -0,0 +1,555 @@ +// 🎨 媒体预览组件逻辑 +const mediaManager = require('../../utils/media-manager.js'); + +Component({ + properties: { + // 是否显示预览 + visible: { + type: Boolean, + value: false + }, + + // 媒体列表 + mediaList: { + type: Array, + value: [] + }, + + // 当前索引 + currentIndex: { + type: Number, + value: 0 + }, + + // 是否可以分享 + canShare: { + type: Boolean, + value: true + }, + + // 是否可以编辑 + canEdit: { + type: Boolean, + value: false + }, + + // 是否可以删除 + canDelete: { + type: Boolean, + value: false + }, + + // 是否显示底部操作栏 + showFooter: { + type: Boolean, + value: true + }, + + // 是否显示手势提示 + showGestureTips: { + type: Boolean, + value: true + } + }, + + data: { + // 当前媒体 + currentMedia: {}, + + // 音频播放状态 + audioPlaying: false, + audioProgress: 0, + audioCurrentTime: 0, + + // 手势提示定时器 + gestureTimer: null + }, + + observers: { + 'mediaList, currentIndex': function(mediaList, currentIndex) { + if (mediaList && mediaList.length > 0 && currentIndex >= 0 && currentIndex < mediaList.length) { + this.setData({ + currentMedia: mediaList[currentIndex] + }); + } + } + }, + + lifetimes: { + attached() { + console.log('🎨 媒体预览组件已加载'); + }, + + detached() { + console.log('🎨 媒体预览组件已卸载'); + this.cleanup(); + } + }, + + methods: { + // 🎨 ===== 基础操作 ===== + + // 阻止事件冒泡 + stopPropagation() { + // 阻止点击事件冒泡到遮罩层 + }, + + // 遮罩点击 + onMaskTap() { + this.closePreview(); + }, + + // 关闭预览 + closePreview() { + this.setData({ + visible: false + }); + + this.triggerEvent('close'); + this.cleanup(); + }, + + // 清理资源 + cleanup() { + // 停止音频播放 + if (this.data.audioPlaying) { + this.stopAudio(); + } + + // 清理定时器 + if (this.data.gestureTimer) { + clearTimeout(this.data.gestureTimer); + } + }, + + // 🎨 ===== 图片操作 ===== + + // 轮播图切换 + onSwiperChange(e) { + const currentIndex = e.detail.current; + this.setData({ + currentIndex: currentIndex + }); + + this.triggerEvent('indexchange', { + currentIndex: currentIndex + }); + }, + + // 图片加载完成 + onImageLoad(e) { + console.log('🖼️ 图片加载完成'); + + const index = e.currentTarget.dataset.index; + const mediaList = this.data.mediaList; + + if (mediaList[index]) { + mediaList[index].loading = false; + mediaList[index].error = false; + + this.setData({ + mediaList: mediaList + }); + } + }, + + // 图片加载失败 + onImageError(e) { + console.error('❌ 图片加载失败'); + + const index = e.currentTarget.dataset.index; + const mediaList = this.data.mediaList; + + if (mediaList[index]) { + mediaList[index].loading = false; + mediaList[index].error = true; + + this.setData({ + mediaList: mediaList + }); + } + }, + + // 图片点击 + onImageTap(e) { + // 可以实现双击放大等功能 + console.log('🖼️ 图片点击'); + }, + + // 重试加载 + retryLoad(e) { + const index = e.currentTarget.dataset.index; + const mediaList = this.data.mediaList; + + if (mediaList[index]) { + mediaList[index].loading = true; + mediaList[index].error = false; + + this.setData({ + mediaList: mediaList + }); + } + }, + + // 🎨 ===== 视频操作 ===== + + // 视频播放 + onVideoPlay() { + console.log('🎬 视频开始播放'); + this.triggerEvent('videoplay'); + }, + + // 视频暂停 + onVideoPause() { + console.log('🎬 视频暂停'); + this.triggerEvent('videopause'); + }, + + // 视频结束 + onVideoEnded() { + console.log('🎬 视频播放结束'); + this.triggerEvent('videoended'); + }, + + // 视频错误 + onVideoError(e) { + console.error('❌ 视频播放错误:', e.detail); + wx.showToast({ + title: '视频播放失败', + icon: 'none' + }); + }, + + // 视频时间更新 + onVideoTimeUpdate(e) { + // 可以用于显示播放进度 + console.log('🎬 视频时间更新:', e.detail); + }, + + // 🎨 ===== 音频操作 ===== + + // 切换音频播放 + toggleAudioPlay() { + if (this.data.audioPlaying) { + this.pauseAudio(); + } else { + this.playAudio(); + } + }, + + // 播放音频 + playAudio() { + // 这里需要实现音频播放逻辑 + console.log('🎵 播放音频'); + + this.setData({ + audioPlaying: true + }); + + // 模拟播放进度 + this.startAudioProgress(); + }, + + // 暂停音频 + pauseAudio() { + console.log('🎵 暂停音频'); + + this.setData({ + audioPlaying: false + }); + + this.stopAudioProgress(); + }, + + // 停止音频 + stopAudio() { + console.log('🎵 停止音频'); + + this.setData({ + audioPlaying: false, + audioProgress: 0, + audioCurrentTime: 0 + }); + + this.stopAudioProgress(); + }, + + // 开始音频进度更新 + startAudioProgress() { + this.audioProgressTimer = setInterval(() => { + const currentTime = this.data.audioCurrentTime + 1; + const duration = this.data.currentMedia.duration || 100; + const progress = (currentTime / duration) * 100; + + this.setData({ + audioCurrentTime: currentTime, + audioProgress: Math.min(progress, 100) + }); + + if (progress >= 100) { + this.stopAudio(); + } + }, 1000); + }, + + // 停止音频进度更新 + stopAudioProgress() { + if (this.audioProgressTimer) { + clearInterval(this.audioProgressTimer); + this.audioProgressTimer = null; + } + }, + + // 🎨 ===== 文件操作 ===== + + // 打开文件 + openFile() { + const currentMedia = this.data.currentMedia; + + wx.openDocument({ + filePath: currentMedia.tempFilePath || currentMedia.url, + fileType: currentMedia.extension, + success: () => { + console.log('📄 文件打开成功'); + }, + fail: (error) => { + console.error('❌ 文件打开失败:', error); + wx.showToast({ + title: '无法打开此文件', + icon: 'none' + }); + } + }); + }, + + // 保存文件 + async saveFile() { + const currentMedia = this.data.currentMedia; + + try { + wx.showLoading({ + title: '保存中...' + }); + + // 如果是网络文件,先下载 + let filePath = currentMedia.tempFilePath; + if (!filePath && currentMedia.url) { + const downloadResult = await mediaManager.downloadFile(currentMedia.url); + if (downloadResult.success) { + filePath = downloadResult.tempFilePath; + } else { + throw new Error('下载失败'); + } + } + + // 保存到本地 + const result = await new Promise((resolve, reject) => { + wx.saveFile({ + tempFilePath: filePath, + success: resolve, + fail: reject + }); + }); + + wx.hideLoading(); + wx.showToast({ + title: '保存成功', + icon: 'success' + }); + + console.log('📄 文件保存成功:', result.savedFilePath); + + } catch (error) { + wx.hideLoading(); + console.error('❌ 文件保存失败:', error); + wx.showToast({ + title: '保存失败', + icon: 'none' + }); + } + }, + + // 🎨 ===== 操作按钮 ===== + + // 下载媒体 + async downloadMedia() { + const currentMedia = this.data.currentMedia; + + if (!currentMedia.url) { + wx.showToast({ + title: '无法下载', + icon: 'none' + }); + return; + } + + try { + wx.showLoading({ + title: '下载中...' + }); + + const result = await mediaManager.downloadFile(currentMedia.url, { + fileName: currentMedia.name + }); + + wx.hideLoading(); + + if (result.success) { + wx.showToast({ + title: '下载完成', + icon: 'success' + }); + + this.triggerEvent('download', { + media: currentMedia, + filePath: result.tempFilePath + }); + } else { + throw new Error(result.error); + } + + } catch (error) { + wx.hideLoading(); + console.error('❌ 下载失败:', error); + wx.showToast({ + title: '下载失败', + icon: 'none' + }); + } + }, + + // 分享媒体 + shareMedia() { + const currentMedia = this.data.currentMedia; + + this.triggerEvent('share', { + media: currentMedia + }); + }, + + // 编辑媒体 + editMedia() { + const currentMedia = this.data.currentMedia; + + this.triggerEvent('edit', { + media: currentMedia, + index: this.data.currentIndex + }); + }, + + // 删除媒体 + deleteMedia() { + const currentMedia = this.data.currentMedia; + + wx.showModal({ + title: '删除确认', + content: '确定要删除这个文件吗?', + success: (res) => { + if (res.confirm) { + this.triggerEvent('delete', { + media: currentMedia, + index: this.data.currentIndex + }); + } + } + }); + }, + + // 收藏媒体 + favoriteMedia() { + const currentMedia = this.data.currentMedia; + const favorited = !currentMedia.favorited; + + // 更新收藏状态 + currentMedia.favorited = favorited; + this.setData({ + currentMedia: currentMedia + }); + + this.triggerEvent('favorite', { + media: currentMedia, + favorited: favorited + }); + + wx.showToast({ + title: favorited ? '已收藏' : '已取消收藏', + icon: 'success' + }); + }, + + // 显示更多操作 + showMoreActions() { + const actions = ['转发', '设为壁纸', '添加到相册', '举报']; + + wx.showActionSheet({ + itemList: actions, + success: (res) => { + this.triggerEvent('moreaction', { + action: actions[res.tapIndex], + media: this.data.currentMedia + }); + } + }); + }, + + // 🎨 ===== 工具方法 ===== + + // 格式化文件大小 + formatFileSize(size) { + if (!size) return '未知大小'; + + const units = ['B', 'KB', 'MB', 'GB']; + let unitIndex = 0; + let fileSize = size; + + while (fileSize >= 1024 && unitIndex < units.length - 1) { + fileSize /= 1024; + unitIndex++; + } + + return `${fileSize.toFixed(1)} ${units[unitIndex]}`; + }, + + // 格式化时长 + formatDuration(duration) { + if (!duration) return '00:00'; + + const minutes = Math.floor(duration / 60); + const seconds = Math.floor(duration % 60); + + return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + }, + + // 格式化时间 + formatTime(time) { + if (!time) return '00:00'; + + const minutes = Math.floor(time / 60); + const seconds = Math.floor(time % 60); + + return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + }, + + // 获取文件图标 + getFileIcon(extension) { + const iconMap = { + 'pdf': '📄', + 'doc': '📝', + 'docx': '📝', + 'xls': '📊', + 'xlsx': '📊', + 'ppt': '📽️', + 'pptx': '📽️', + 'txt': '📃', + 'zip': '🗜️', + 'rar': '🗜️', + 'mp3': '🎵', + 'wav': '🎵', + 'mp4': '🎬', + 'avi': '🎬' + }; + + return iconMap[extension] || '📄'; + } + } +}); diff --git a/components/media-preview/media-preview.json b/components/media-preview/media-preview.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/components/media-preview/media-preview.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/components/media-preview/media-preview.wxml b/components/media-preview/media-preview.wxml new file mode 100644 index 0000000..aec88a4 --- /dev/null +++ b/components/media-preview/media-preview.wxml @@ -0,0 +1,192 @@ + + + + + + + + + + + {{currentMedia.name || '媒体预览'}} + + {{formatFileSize(currentMedia.size)}} + + + + + + + 📥 + + + + + 📤 + + + + + + + + + + + + + + + + + + + + + + 加载中... + + + + + + 加载失败 + + 重试 + + + + + + + + + {{currentIndex + 1}} / {{mediaList.length}} + + + + + + + + + + {{formatDuration(currentMedia.duration)}} + {{currentMedia.width}}×{{currentMedia.height}} + + + + + + + {{getFileIcon(currentMedia.extension)}} + + + + {{currentMedia.name}} + {{formatFileSize(currentMedia.size)}} + {{currentMedia.extension.toUpperCase()}} 文件 + + + + + 打开文件 + + + 保存到本地 + + + + + + + + + 🎵 + + + + + {{audioPlaying ? '⏸️' : '▶️'}} + + + + + + + + {{formatTime(audioCurrentTime)}} + {{formatTime(currentMedia.duration)}} + + + + + + + {{currentMedia.name}} + {{formatFileSize(currentMedia.size)}} + + + + + + + + + + ✏️ + 编辑 + + + + + 🗑️ + 删除 + + + + + {{currentMedia.favorited ? '❤️' : '🤍'}} + {{currentMedia.favorited ? '已收藏' : '收藏'}} + + + + + + 更多 + + + + + + + + 双击放大 · 滑动切换 · 点击关闭 + + diff --git a/components/media-preview/media-preview.wxss b/components/media-preview/media-preview.wxss new file mode 100644 index 0000000..8164f7f --- /dev/null +++ b/components/media-preview/media-preview.wxss @@ -0,0 +1,586 @@ +/* 🎨 媒体预览组件样式 */ + +/* CSS变量定义 */ +.media-preview-container { + --preview-bg: rgba(0, 0, 0, 0.9); + --header-bg: rgba(0, 0, 0, 0.7); + --text-primary: #FFFFFF; + --text-secondary: rgba(255, 255, 255, 0.7); + --button-bg: rgba(255, 255, 255, 0.1); + --button-active: rgba(255, 255, 255, 0.2); + --border-color: rgba(255, 255, 255, 0.2); + --shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.3); +} + +/* 🎨 预览容器 */ +.media-preview-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + display: flex; + flex-direction: column; + background: var(--preview-bg); + animation: fadeIn 0.3s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* 🎨 背景遮罩 */ +.preview-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: transparent; +} + +/* 🎨 预览内容 */ +.preview-content { + flex: 1; + display: flex; + flex-direction: column; + position: relative; + z-index: 1; +} + +/* 🎨 头部工具栏 */ +.preview-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32rpx; + background: var(--header-bg); + backdrop-filter: blur(20rpx); + border-bottom: 1rpx solid var(--border-color); +} + +.header-info { + flex: 1; + min-width: 0; +} + +.media-title { + display: block; + font-size: 32rpx; + font-weight: 600; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 8rpx; +} + +.media-info { + font-size: 26rpx; + color: var(--text-secondary); +} + +.header-actions { + display: flex; + align-items: center; + gap: 16rpx; +} + +.action-btn { + width: 72rpx; + height: 72rpx; + border-radius: 36rpx; + background: var(--button-bg); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + backdrop-filter: blur(10rpx); +} + +.action-btn:active { + background: var(--button-active); + transform: scale(0.9); +} + +.action-btn.close-btn { + background: rgba(255, 59, 48, 0.8); +} + +.action-icon { + font-size: 32rpx; + color: var(--text-primary); +} + +/* 🎨 媒体容器 */ +.media-container { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +/* 🎨 图片预览 */ +.image-preview { + width: 100%; + height: 100%; + position: relative; +} + +.image-swiper { + width: 100%; + height: 100%; +} + +.image-item { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.preview-image { + max-width: 100%; + max-height: 100%; + border-radius: 16rpx; + box-shadow: var(--shadow); +} + +.image-counter { + position: absolute; + bottom: 40rpx; + left: 50%; + transform: translateX(-50%); + padding: 12rpx 24rpx; + background: var(--header-bg); + border-radius: 24rpx; + backdrop-filter: blur(20rpx); +} + +.counter-text { + font-size: 28rpx; + color: var(--text-primary); + font-weight: 500; +} + +/* 🎨 视频预览 */ +.video-preview { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; +} + +.preview-video { + width: 100%; + max-height: 80%; + border-radius: 16rpx; + box-shadow: var(--shadow); +} + +.video-info { + position: absolute; + bottom: 40rpx; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 24rpx; + padding: 12rpx 24rpx; + background: var(--header-bg); + border-radius: 24rpx; + backdrop-filter: blur(20rpx); +} + +.video-duration, +.video-size { + font-size: 26rpx; + color: var(--text-secondary); +} + +/* 🎨 文件预览 */ +.file-preview { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80rpx 40rpx; + text-align: center; +} + +.file-icon-container { + width: 200rpx; + height: 200rpx; + border-radius: 32rpx; + background: var(--button-bg); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 40rpx; + backdrop-filter: blur(20rpx); + border: 2rpx solid var(--border-color); +} + +.file-icon { + font-size: 120rpx; +} + +.file-details { + margin-bottom: 60rpx; +} + +.file-name { + display: block; + font-size: 36rpx; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 16rpx; + word-break: break-word; +} + +.file-size, +.file-type { + display: block; + font-size: 28rpx; + color: var(--text-secondary); + margin-bottom: 8rpx; +} + +.file-actions { + display: flex; + gap: 24rpx; +} + +.file-action-btn { + padding: 24rpx 48rpx; + background: var(--button-bg); + border-radius: 32rpx; + border: 1rpx solid var(--border-color); + transition: all 0.3s ease; + backdrop-filter: blur(20rpx); +} + +.file-action-btn:active { + background: var(--button-active); + transform: scale(0.95); +} + +.action-text { + font-size: 30rpx; + color: var(--text-primary); + font-weight: 500; +} + +/* 🎨 音频预览 */ +.audio-preview { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80rpx 40rpx; +} + +.audio-player { + display: flex; + flex-direction: column; + align-items: center; + gap: 40rpx; + margin-bottom: 60rpx; +} + +.audio-cover { + width: 200rpx; + height: 200rpx; + border-radius: 100rpx; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + box-shadow: var(--shadow); +} + +.audio-icon { + font-size: 80rpx; + color: white; +} + +.audio-controls { + display: flex; + align-items: center; + gap: 32rpx; + width: 100%; + max-width: 600rpx; +} + +.play-btn { + width: 96rpx; + height: 96rpx; + border-radius: 48rpx; + background: var(--button-bg); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + backdrop-filter: blur(20rpx); + border: 2rpx solid var(--border-color); +} + +.play-btn:active { + background: var(--button-active); + transform: scale(0.9); +} + +.play-btn.playing { + background: rgba(52, 199, 89, 0.8); +} + +.play-icon { + font-size: 40rpx; + color: var(--text-primary); +} + +.audio-progress { + flex: 1; +} + +.progress-bar { + height: 8rpx; + background: var(--button-bg); + border-radius: 4rpx; + margin-bottom: 16rpx; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + border-radius: 4rpx; + transition: width 0.3s ease; +} + +.time-info { + display: flex; + justify-content: space-between; +} + +.current-time, +.total-time { + font-size: 24rpx; + color: var(--text-secondary); +} + +.audio-info { + text-align: center; +} + +.audio-name { + display: block; + font-size: 32rpx; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 12rpx; + word-break: break-word; +} + +.audio-size { + font-size: 26rpx; + color: var(--text-secondary); +} + +/* 🎨 加载和错误状态 */ +.loading-overlay, +.error-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: var(--preview-bg); + border-radius: 16rpx; +} + +.loading-spinner { + width: 60rpx; + height: 60rpx; + border: 4rpx solid var(--border-color); + border-top: 4rpx solid var(--text-primary); + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 24rpx; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-text, +.error-text { + font-size: 28rpx; + color: var(--text-secondary); + margin-bottom: 24rpx; +} + +.error-icon { + font-size: 80rpx; + margin-bottom: 24rpx; +} + +.retry-btn { + padding: 16rpx 32rpx; + background: var(--button-bg); + border-radius: 24rpx; + border: 1rpx solid var(--border-color); + transition: all 0.3s ease; +} + +.retry-btn:active { + background: var(--button-active); + transform: scale(0.95); +} + +.retry-text { + font-size: 26rpx; + color: var(--text-primary); +} + +/* 🎨 底部操作栏 */ +.preview-footer { + padding: 32rpx; + background: var(--header-bg); + backdrop-filter: blur(20rpx); + border-top: 1rpx solid var(--border-color); +} + +.footer-actions { + display: flex; + justify-content: space-around; + align-items: center; +} + +.footer-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; + padding: 16rpx; + border-radius: 16rpx; + transition: all 0.3s ease; + min-width: 120rpx; +} + +.footer-btn:active { + background: var(--button-bg); + transform: scale(0.95); +} + +.footer-icon { + font-size: 32rpx; +} + +.footer-text { + font-size: 24rpx; + color: var(--text-secondary); +} + +/* 🎨 手势提示 */ +.gesture-tips { + position: absolute; + bottom: 160rpx; + left: 50%; + transform: translateX(-50%); + padding: 16rpx 32rpx; + background: var(--header-bg); + border-radius: 32rpx; + backdrop-filter: blur(20rpx); + animation: tipsFadeIn 0.5s ease-out 1s both; +} + +@keyframes tipsFadeIn { + from { + opacity: 0; + transform: translateX(-50%) translateY(20rpx); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} + +.tips-text { + font-size: 24rpx; + color: var(--text-secondary); + text-align: center; +} + +/* 📱 响应式设计 */ +@media screen and (max-width: 375px) { + .preview-header { + padding: 24rpx; + } + + .media-title { + font-size: 28rpx; + } + + .action-btn { + width: 64rpx; + height: 64rpx; + } + + .action-icon { + font-size: 28rpx; + } + + .file-icon-container { + width: 160rpx; + height: 160rpx; + } + + .file-icon { + font-size: 96rpx; + } +} + +@media screen and (min-width: 414px) { + .preview-header { + padding: 40rpx; + } + + .media-title { + font-size: 36rpx; + } + + .action-btn { + width: 80rpx; + height: 80rpx; + } + + .action-icon { + font-size: 36rpx; + } + + .file-icon-container { + width: 240rpx; + height: 240rpx; + } + + .file-icon { + font-size: 140rpx; + } +} diff --git a/components/mention-selector/mention-selector.js b/components/mention-selector/mention-selector.js new file mode 100644 index 0000000..2dc83ee --- /dev/null +++ b/components/mention-selector/mention-selector.js @@ -0,0 +1,181 @@ +// 💬 @提醒选择组件逻辑 +const groupChatManager = require('../../utils/group-chat-manager.js'); + +Component({ + properties: { + // 是否显示 + visible: { + type: Boolean, + value: false + }, + + // 群ID + groupId: { + type: String, + value: '' + }, + + // 当前用户ID + currentUserId: { + type: String, + value: '' + }, + + // 是否显示@全体成员 + showMentionAll: { + type: Boolean, + value: true + } + }, + + data: { + // 成员数据 + allMembers: [], + filteredMembers: [], + + // 搜索关键词 + searchKeyword: '', + + // 加载状态 + loading: false + }, + + observers: { + 'visible': function(visible) { + if (visible && this.data.groupId) { + this.loadGroupMembers(); + } + }, + + 'groupId': function(groupId) { + if (groupId && this.data.visible) { + this.loadGroupMembers(); + } + } + }, + + methods: { + // 加载群成员 + async loadGroupMembers() { + if (!this.data.groupId) return; + + try { + this.setData({ + loading: true + }); + + const result = await groupChatManager.getGroupMembers(this.data.groupId); + + if (result.success) { + // 过滤掉当前用户 + const members = result.data.filter(member => member.userId !== this.data.currentUserId); + + this.setData({ + allMembers: members, + loading: false + }); + + // 应用搜索过滤 + this.applyFilter(); + + console.log('✅ 群成员加载完成:', members.length); + } else { + throw new Error(result.error || '获取群成员失败'); + } + + } catch (error) { + this.setData({ + loading: false + }); + + console.error('❌ 加载群成员失败:', error); + wx.showToast({ + title: '加载成员失败', + icon: 'none' + }); + } + }, + + // 搜索输入 + onSearchInput(e) { + const keyword = e.detail.value; + this.setData({ + searchKeyword: keyword + }); + this.applyFilter(); + }, + + // 清除搜索 + clearSearch() { + this.setData({ + searchKeyword: '' + }); + this.applyFilter(); + }, + + // 应用搜索过滤 + applyFilter() { + const keyword = this.data.searchKeyword.toLowerCase(); + let filtered = this.data.allMembers; + + if (keyword) { + filtered = this.data.allMembers.filter(member => { + const name = (member.nickname || member.username || '').toLowerCase(); + return name.includes(keyword); + }); + } + + this.setData({ + filteredMembers: filtered + }); + }, + + // @全体成员 + onMentionAll() { + console.log('💬 @全体成员'); + + this.triggerEvent('mention', { + type: 'all', + text: '所有人', + userIds: this.data.allMembers.map(member => member.userId) + }); + + this.onClose(); + }, + + // @特定成员 + onMentionMember(e) { + const member = e.currentTarget.dataset.member; + console.log('💬 @特定成员:', member); + + this.triggerEvent('mention', { + type: 'user', + text: member.nickname || member.username, + userId: member.userId, + userIds: [member.userId] + }); + + this.onClose(); + }, + + // 关闭选择器 + onClose() { + this.setData({ + searchKeyword: '', + filteredMembers: this.data.allMembers + }); + + this.triggerEvent('close'); + }, + + // 点击遮罩 + onMaskTap() { + this.onClose(); + }, + + // 阻止事件冒泡 + stopPropagation() { + // 阻止点击事件冒泡 + } + } +}); diff --git a/components/mention-selector/mention-selector.json b/components/mention-selector/mention-selector.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/components/mention-selector/mention-selector.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/components/mention-selector/mention-selector.wxml b/components/mention-selector/mention-selector.wxml new file mode 100644 index 0000000..be61dd8 --- /dev/null +++ b/components/mention-selector/mention-selector.wxml @@ -0,0 +1,85 @@ + + + + + + 选择要@的成员 + + + + + + + + + 🔍 + + + + + + + + + + + + + + @ + + + + + 所有人 + @全体成员 + + + + @ + + + + + + + + + 群主 + + + 管理员 + + + + + {{item.nickname || item.username}} + {{item.status || ''}} + + + + @ + + + + + + 👥 + 没有找到相关成员 + + + + diff --git a/components/mention-selector/mention-selector.wxss b/components/mention-selector/mention-selector.wxss new file mode 100644 index 0000000..96b824c --- /dev/null +++ b/components/mention-selector/mention-selector.wxss @@ -0,0 +1,378 @@ +/* 💬 @提醒选择组件样式 */ + +/* CSS变量定义 */ +:host { + --primary-color: #007AFF; + --primary-light: #5AC8FA; + --success-color: #34C759; + --warning-color: #FF9500; + --background-color: #F2F2F7; + --surface-color: #FFFFFF; + --text-primary: #000000; + --text-secondary: #8E8E93; + --text-tertiary: #C7C7CC; + --border-color: #E5E5EA; + --shadow-light: 0 1rpx 3rpx rgba(0, 0, 0, 0.1); + --shadow-medium: 0 4rpx 12rpx rgba(0, 0, 0, 0.15); + --radius-small: 8rpx; + --radius-medium: 12rpx; + --radius-large: 20rpx; +} + +/* 🌙 深色模式支持 */ +@media (prefers-color-scheme: dark) { + :host { + --primary-color: #0A84FF; + --primary-light: #64D2FF; + --success-color: #30D158; + --warning-color: #FF9F0A; + --background-color: #000000; + --surface-color: #1C1C1E; + --text-primary: #FFFFFF; + --text-secondary: #8E8E93; + --text-tertiary: #48484A; + --border-color: #38383A; + --shadow-light: 0 1rpx 3rpx rgba(0, 0, 0, 0.3); + --shadow-medium: 0 4rpx 12rpx rgba(0, 0, 0, 0.4); + } +} + +.mention-selector-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: flex-end; + animation: fadeIn 0.3s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.selector-content { + width: 100%; + max-height: 80vh; + background: var(--surface-color); + border-radius: var(--radius-large) var(--radius-large) 0 0; + box-shadow: var(--shadow-medium); + animation: slideUp 0.3s ease-out; + display: flex; + flex-direction: column; + overflow: hidden; +} + +@keyframes slideUp { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +/* 🎨 选择器头部 */ +.selector-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32rpx; + border-bottom: 1rpx solid var(--border-color); + background: var(--background-color); +} + +.header-title { + font-size: 36rpx; + font-weight: 600; + color: var(--text-primary); +} + +.close-btn { + width: 64rpx; + height: 64rpx; + border-radius: 32rpx; + background: var(--surface-color); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.close-btn:active { + background: var(--border-color); + transform: scale(0.9); +} + +.close-icon { + font-size: 28rpx; + color: var(--text-secondary); +} + +/* 🎨 搜索框 */ +.search-container { + padding: 24rpx 32rpx; + background: var(--surface-color); + border-bottom: 1rpx solid var(--border-color); +} + +.search-input-wrapper { + display: flex; + align-items: center; + background: var(--background-color); + border: 1rpx solid var(--border-color); + border-radius: var(--radius-small); + padding: 0 24rpx; + transition: all 0.3s ease; +} + +.search-input-wrapper:focus-within { + border-color: var(--primary-color); + box-shadow: 0 0 0 4rpx rgba(0, 122, 255, 0.1); +} + +.search-icon { + font-size: 28rpx; + color: var(--text-secondary); + margin-right: 16rpx; +} + +.search-input { + flex: 1; + height: 80rpx; + font-size: 28rpx; + color: var(--text-primary); +} + +.clear-search { + width: 48rpx; + height: 48rpx; + border-radius: 24rpx; + background: var(--text-tertiary); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.clear-search:active { + transform: scale(0.9); +} + +.clear-icon { + font-size: 24rpx; + color: white; +} + +/* 🎨 成员列表 */ +.members-list { + flex: 1; + background: var(--surface-color); +} + +.member-item { + display: flex; + align-items: center; + padding: 24rpx 32rpx; + border-bottom: 1rpx solid var(--border-color); + transition: all 0.2s ease; +} + +.member-item:last-child { + border-bottom: none; +} + +.member-item:active { + background: var(--background-color); +} + +.member-item.mention-all { + background: rgba(0, 122, 255, 0.05); +} + +.member-item.mention-all:active { + background: rgba(0, 122, 255, 0.1); +} + +.member-avatar-container { + position: relative; + margin-right: 24rpx; +} + +.member-avatar { + width: 80rpx; + height: 80rpx; + border-radius: 40rpx; + border: 2rpx solid var(--border-color); +} + +.mention-all-avatar { + width: 80rpx; + height: 80rpx; + border-radius: 40rpx; + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%); + display: flex; + align-items: center; + justify-content: center; + border: 2rpx solid var(--border-color); +} + +.mention-all-icon { + font-size: 36rpx; + color: white; + font-weight: bold; +} + +.role-badge { + position: absolute; + bottom: -6rpx; + right: -6rpx; + padding: 4rpx 8rpx; + border-radius: 12rpx; + border: 2rpx solid var(--surface-color); +} + +.role-badge.owner { + background: var(--warning-color); +} + +.role-badge.admin { + background: var(--primary-color); +} + +.role-text { + font-size: 20rpx; + color: white; + font-weight: 600; +} + +.member-info { + flex: 1; + min-width: 0; +} + +.member-name { + font-size: 30rpx; + font-weight: 500; + color: var(--text-primary); + display: block; + margin-bottom: 8rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.member-desc { + font-size: 26rpx; + color: var(--text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.member-action { + display: flex; + align-items: center; + justify-content: center; + width: 64rpx; + height: 64rpx; + border-radius: 32rpx; + background: var(--primary-color); + transition: all 0.2s ease; +} + +.member-action:active { + transform: scale(0.9); +} + +.action-text { + font-size: 28rpx; + color: white; + font-weight: bold; +} + +/* 🎨 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80rpx 40rpx; + text-align: center; +} + +.empty-icon { + font-size: 120rpx; + margin-bottom: 24rpx; + opacity: 0.5; +} + +.empty-text { + font-size: 28rpx; + color: var(--text-secondary); +} + +/* 📱 响应式设计 */ +@media screen and (max-width: 375px) { + .selector-header, + .search-container, + .member-item { + padding-left: 24rpx; + padding-right: 24rpx; + } + + .member-avatar, + .mention-all-avatar { + width: 64rpx; + height: 64rpx; + border-radius: 32rpx; + } + + .mention-all-icon { + font-size: 28rpx; + } + + .member-action { + width: 48rpx; + height: 48rpx; + border-radius: 24rpx; + } + + .action-text { + font-size: 24rpx; + } +} + +@media screen and (min-width: 414px) { + .selector-header, + .search-container, + .member-item { + padding-left: 40rpx; + padding-right: 40rpx; + } + + .member-avatar, + .mention-all-avatar { + width: 96rpx; + height: 96rpx; + border-radius: 48rpx; + } + + .mention-all-icon { + font-size: 40rpx; + } + + .member-action { + width: 80rpx; + height: 80rpx; + border-radius: 40rpx; + } + + .action-text { + font-size: 32rpx; + } +} diff --git a/components/message-action-menu/message-action-menu.js b/components/message-action-menu/message-action-menu.js new file mode 100644 index 0000000..1d9955d --- /dev/null +++ b/components/message-action-menu/message-action-menu.js @@ -0,0 +1,546 @@ +// ✨ 消息操作菜单组件逻辑 +const messageInteractionManager = require('../../utils/message-interaction-manager.js'); + +Component({ + properties: { + // 是否显示菜单 + visible: { + type: Boolean, + value: false + }, + + // 消息对象 + message: { + type: Object, + value: {} + }, + + // 是否是自己的消息 + isOwnMessage: { + type: Boolean, + value: false + }, + + // 可用的操作 + actions: { + type: Object, + value: { + quote: true, // 引用回复 + forward: true, // 转发 + favorite: true, // 收藏 + multiSelect: true, // 多选 + copy: true, // 复制 + recall: true, // 撤回 + delete: true, // 删除 + report: true // 举报 + } + }, + + // 是否显示表情回应 + showReactions: { + type: Boolean, + value: true + }, + + // 是否显示消息信息 + showMessageInfo: { + type: Boolean, + value: false + } + }, + + data: { + // 常用表情 + commonEmojis: ['👍', '❤️', '😂', '😮', '😢', '😡'], + + // 是否可以撤回 + canRecall: false, + + // 表情选择器 + showEmojiPicker: false, + currentEmojiCategory: 'recent', + currentEmojiList: [], + + // 表情分类 + emojiCategories: { + recent: ['👍', '❤️', '😂', '😮', '😢', '😡', '🎉', '🔥'], + smileys: ['😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇', '🙂', '🙃', '😉', '😌', '😍', '🥰', '😘', '😗', '😙', '😚', '😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🤩', '🥳'], + gestures: ['👍', '👎', '👌', '✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆', '🖕', '👇', '☝️', '👋', '🤚', '🖐️', '✋', '🖖', '👏', '🙌', '🤲', '🤝', '🙏'], + hearts: ['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '💟'] + } + }, + + observers: { + 'message, isOwnMessage': function(message, isOwnMessage) { + if (message && message.messageId) { + this.checkRecallPermission(); + } + } + }, + + lifetimes: { + attached() { + console.log('✨ 消息操作菜单组件已加载'); + this.initEmojiList(); + } + }, + + methods: { + // ✨ ===== 基础操作 ===== + + // 阻止事件冒泡 + stopPropagation() { + // 阻止点击事件冒泡到遮罩层 + }, + + // 遮罩点击 + onMaskTap() { + this.closeMenu(); + }, + + // 关闭菜单 + closeMenu() { + this.setData({ + visible: false, + showEmojiPicker: false + }); + + this.triggerEvent('close'); + }, + + // 👍 ===== 表情回应操作 ===== + + // 表情点击 + async onReactionTap(e) { + const emoji = e.currentTarget.dataset.emoji; + console.log('👍 表情点击:', emoji); + + try { + const userId = wx.getStorageSync('userId'); + if (!userId) { + wx.showToast({ + title: '请先登录', + icon: 'none' + }); + return; + } + + // 添加表情回应 + const result = await messageInteractionManager.addReaction( + this.data.message.messageId, + emoji, + userId + ); + + if (result.success) { + // 触发表情回应事件 + this.triggerEvent('reaction', { + messageId: this.data.message.messageId, + emoji: emoji, + action: 'add' + }); + + // 关闭菜单 + this.closeMenu(); + + wx.showToast({ + title: '表情回应已添加', + icon: 'success' + }); + } else { + wx.showToast({ + title: result.error || '添加失败', + icon: 'none' + }); + } + + } catch (error) { + console.error('❌ 添加表情回应失败:', error); + wx.showToast({ + title: '操作失败', + icon: 'none' + }); + } + }, + + // 显示更多表情 + showMoreEmojis() { + this.setData({ + showEmojiPicker: true, + currentEmojiCategory: 'recent' + }); + this.updateEmojiList(); + }, + + // 关闭表情选择器 + closeEmojiPicker() { + this.setData({ + showEmojiPicker: false + }); + }, + + // 切换表情分类 + switchEmojiCategory(e) { + const category = e.currentTarget.dataset.category; + this.setData({ + currentEmojiCategory: category + }); + this.updateEmojiList(); + }, + + // 表情选择 + async onEmojiSelect(e) { + const emoji = e.currentTarget.dataset.emoji; + + // 添加到最近使用 + this.addToRecentEmojis(emoji); + + // 执行表情回应 + await this.onReactionTap({ currentTarget: { dataset: { emoji } } }); + }, + + // 初始化表情列表 + initEmojiList() { + this.setData({ + currentEmojiList: this.data.emojiCategories.recent + }); + }, + + // 更新表情列表 + updateEmojiList() { + const category = this.data.currentEmojiCategory; + const emojiList = this.data.emojiCategories[category] || []; + + this.setData({ + currentEmojiList: emojiList + }); + }, + + // 添加到最近使用表情 + addToRecentEmojis(emoji) { + let recentEmojis = [...this.data.emojiCategories.recent]; + + // 移除已存在的 + recentEmojis = recentEmojis.filter(e => e !== emoji); + + // 添加到开头 + recentEmojis.unshift(emoji); + + // 限制数量 + if (recentEmojis.length > 20) { + recentEmojis = recentEmojis.slice(0, 20); + } + + // 更新数据 + this.setData({ + [`emojiCategories.recent`]: recentEmojis + }); + + // 如果当前显示的是最近分类,更新列表 + if (this.data.currentEmojiCategory === 'recent') { + this.setData({ + currentEmojiList: recentEmojis + }); + } + }, + + // 🎯 ===== 操作按钮处理 ===== + + // 操作点击 + async onActionTap(e) { + const action = e.currentTarget.dataset.action; + console.log('🎯 操作点击:', action); + + switch (action) { + case 'quote': + this.handleQuote(); + break; + case 'forward': + this.handleForward(); + break; + case 'favorite': + this.handleFavorite(); + break; + case 'multiSelect': + this.handleMultiSelect(); + break; + case 'copy': + this.handleCopy(); + break; + case 'recall': + this.handleRecall(); + break; + case 'delete': + this.handleDelete(); + break; + case 'report': + this.handleReport(); + break; + default: + console.warn('⚠️ 未知操作:', action); + } + }, + + // 处理引用回复 + handleQuote() { + console.log('💬 处理引用回复'); + + this.triggerEvent('action', { + action: 'quote', + message: this.data.message + }); + + this.closeMenu(); + }, + + // 处理转发 + handleForward() { + console.log('📤 处理转发'); + + this.triggerEvent('action', { + action: 'forward', + message: this.data.message + }); + + this.closeMenu(); + }, + + // 处理收藏 + async handleFavorite() { + console.log('⭐ 处理收藏'); + + try { + const userId = wx.getStorageSync('userId'); + const messageId = this.data.message.messageId; + const isFavorited = this.data.message.favorited; + + let result; + if (isFavorited) { + result = await messageInteractionManager.unfavoriteMessage(messageId, userId); + } else { + result = await messageInteractionManager.favoriteMessage(messageId, userId); + } + + if (result.success) { + this.triggerEvent('action', { + action: 'favorite', + message: this.data.message, + favorited: !isFavorited + }); + + wx.showToast({ + title: isFavorited ? '已取消收藏' : '已收藏', + icon: 'success' + }); + } else { + wx.showToast({ + title: result.error || '操作失败', + icon: 'none' + }); + } + + } catch (error) { + console.error('❌ 收藏操作失败:', error); + wx.showToast({ + title: '操作失败', + icon: 'none' + }); + } + + this.closeMenu(); + }, + + // 处理多选 + handleMultiSelect() { + console.log('📋 处理多选'); + + this.triggerEvent('action', { + action: 'multiSelect', + message: this.data.message + }); + + this.closeMenu(); + }, + + // 处理复制 + handleCopy() { + console.log('📄 处理复制'); + + if (this.data.message.msgType === 'text') { + wx.setClipboardData({ + data: this.data.message.content, + success: () => { + wx.showToast({ + title: '已复制到剪贴板', + icon: 'success' + }); + } + }); + } + + this.closeMenu(); + }, + + // 处理撤回 + async handleRecall() { + console.log('🔄 处理撤回'); + + try { + const userId = wx.getStorageSync('userId'); + const messageId = this.data.message.messageId; + + const result = await messageInteractionManager.recallMessage(messageId, userId); + + if (result.success) { + this.triggerEvent('action', { + action: 'recall', + message: this.data.message + }); + + wx.showToast({ + title: '消息已撤回', + icon: 'success' + }); + } else { + wx.showToast({ + title: result.error || '撤回失败', + icon: 'none' + }); + } + + } catch (error) { + console.error('❌ 撤回消息失败:', error); + wx.showToast({ + title: '撤回失败', + icon: 'none' + }); + } + + this.closeMenu(); + }, + + // 处理删除 + handleDelete() { + console.log('🗑️ 处理删除'); + + wx.showModal({ + title: '删除消息', + content: '确定要删除这条消息吗?', + success: (res) => { + if (res.confirm) { + this.triggerEvent('action', { + action: 'delete', + message: this.data.message + }); + } + } + }); + + this.closeMenu(); + }, + + // 处理举报 + handleReport() { + console.log('⚠️ 处理举报'); + + wx.showActionSheet({ + itemList: ['垃圾信息', '违法违规', '色情内容', '暴力内容', '其他'], + success: (res) => { + const reasons = ['spam', 'illegal', 'sexual', 'violence', 'other']; + const reason = reasons[res.tapIndex]; + + this.triggerEvent('action', { + action: 'report', + message: this.data.message, + reason: reason + }); + } + }); + + this.closeMenu(); + }, + + // 🔧 ===== 工具方法 ===== + + // 检查撤回权限 + async checkRecallPermission() { + try { + const userId = wx.getStorageSync('userId'); + const messageId = this.data.message.messageId; + + if (!userId || !messageId) { + this.setData({ canRecall: false }); + return; + } + + const result = await messageInteractionManager.checkRecallPermission(messageId, userId); + this.setData({ canRecall: result.allowed }); + + } catch (error) { + console.error('❌ 检查撤回权限失败:', error); + this.setData({ canRecall: false }); + } + }, + + // 格式化时间 + formatTime(timestamp) { + if (!timestamp) return ''; + + const date = new Date(timestamp); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) { + // 今天 + return date.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit' + }); + } else if (diffDays === 1) { + // 昨天 + return '昨天 ' + date.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit' + }); + } else { + // 更早 + return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit' + }); + } + }, + + // 获取消息类型文本 + getMessageTypeText(msgType) { + const typeMap = { + 'text': '文本', + 'image': '图片', + 'video': '视频', + 'voice': '语音', + 'file': '文件', + 'location': '位置', + 'card': '名片' + }; + + return typeMap[msgType] || '未知'; + }, + + // 格式化文件大小 + formatFileSize(size) { + if (!size) return ''; + + const units = ['B', 'KB', 'MB', 'GB']; + let unitIndex = 0; + let fileSize = size; + + while (fileSize >= 1024 && unitIndex < units.length - 1) { + fileSize /= 1024; + unitIndex++; + } + + return `${fileSize.toFixed(1)} ${units[unitIndex]}`; + } + } +}); diff --git a/components/message-action-menu/message-action-menu.json b/components/message-action-menu/message-action-menu.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/components/message-action-menu/message-action-menu.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/components/message-action-menu/message-action-menu.wxml b/components/message-action-menu/message-action-menu.wxml new file mode 100644 index 0000000..fb869cb --- /dev/null +++ b/components/message-action-menu/message-action-menu.wxml @@ -0,0 +1,191 @@ + + + + + + + + + + + 添加表情回应 + + + + + {{item}} + + + + + + + + + + + + + + + 💬 + + 引用 + + + + + + 📤 + + 转发 + + + + + + {{message.favorited ? '⭐' : '☆'}} + + {{message.favorited ? '取消收藏' : '收藏'}} + + + + + + 📋 + + 多选 + + + + + + 📄 + + 复制 + + + + + + 🔄 + + 撤回 + + + + + + 🗑️ + + 删除 + + + + + + ⚠️ + + 举报 + + + + + + + 发送时间: + {{formatTime(message.timestamp)}} + + + + 编辑时间: + {{formatTime(message.editedAt)}} + + + + 消息类型: + {{getMessageTypeText(message.msgType)}} + + + + 文件大小: + {{formatFileSize(message.size)}} + + + + + + + + + + 选择表情 + + + + + + + + 最近 + + + 笑脸 + + + 手势 + + + 爱心 + + + + + + + {{item}} + + + + + diff --git a/components/message-action-menu/message-action-menu.wxss b/components/message-action-menu/message-action-menu.wxss new file mode 100644 index 0000000..5d8e427 --- /dev/null +++ b/components/message-action-menu/message-action-menu.wxss @@ -0,0 +1,446 @@ +/* ✨ 消息操作菜单组件样式 */ + +/* CSS变量定义 */ +.message-action-menu { + --menu-bg: rgba(0, 0, 0, 0.8); + --content-bg: #FFFFFF; + --border-color: #E5E5EA; + --text-primary: #000000; + --text-secondary: #8E8E93; + --text-danger: #FF3B30; + --button-bg: #F2F2F7; + --button-active: #E5E5EA; + --shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2); + --radius: 16rpx; +} + +/* 🌙 深色模式支持 */ +@media (prefers-color-scheme: dark) { + .message-action-menu { + --content-bg: #1C1C1E; + --border-color: #38383A; + --text-primary: #FFFFFF; + --text-secondary: #8E8E93; + --text-danger: #FF453A; + --button-bg: #2C2C2E; + --button-active: #3A3A3C; + --shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.4); + } +} + +/* 🎨 菜单容器 */ +.message-action-menu { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9998; + display: flex; + align-items: flex-end; + justify-content: center; + animation: fadeIn 0.3s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* 🎨 背景遮罩 */ +.menu-mask { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--menu-bg); +} + +/* 🎨 菜单内容 */ +.menu-content { + width: 100%; + max-width: 750rpx; + background: var(--content-bg); + border-radius: var(--radius) var(--radius) 0 0; + box-shadow: var(--shadow); + animation: slideUp 0.3s ease-out; + overflow: hidden; +} + +@keyframes slideUp { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +/* 🎨 表情回应区域 */ +.reactions-section { + padding: 32rpx; + border-bottom: 1rpx solid var(--border-color); +} + +.reactions-title { + margin-bottom: 24rpx; +} + +.title-text { + font-size: 32rpx; + font-weight: 600; + color: var(--text-primary); +} + +.reactions-grid { + display: flex; + flex-wrap: wrap; + gap: 16rpx; +} + +.reaction-item { + width: 88rpx; + height: 88rpx; + border-radius: 44rpx; + background: var(--button-bg); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + border: 2rpx solid transparent; +} + +.reaction-item:active { + background: var(--button-active); + transform: scale(0.9); +} + +.reaction-emoji { + font-size: 48rpx; +} + +.more-emoji { + border: 2rpx dashed var(--border-color); + background: transparent; +} + +.more-icon { + font-size: 32rpx; + color: var(--text-secondary); +} + +/* 🎨 操作按钮区域 */ +.actions-section { + padding: 16rpx 0; +} + +.action-item { + display: flex; + align-items: center; + padding: 24rpx 32rpx; + transition: all 0.2s ease; + border-bottom: 1rpx solid var(--border-color); +} + +.action-item:last-child { + border-bottom: none; +} + +.action-item:active { + background: var(--button-bg); +} + +.action-item.danger { + color: var(--text-danger); +} + +.action-item.danger .action-text { + color: var(--text-danger); +} + +.action-icon { + width: 72rpx; + height: 72rpx; + border-radius: 36rpx; + background: var(--button-bg); + display: flex; + align-items: center; + justify-content: center; + margin-right: 24rpx; +} + +.action-item.danger .action-icon { + background: rgba(255, 59, 48, 0.1); +} + +.icon-text { + font-size: 32rpx; +} + +.action-text { + font-size: 32rpx; + color: var(--text-primary); + font-weight: 500; +} + +/* 🎨 消息信息区域 */ +.message-info-section { + padding: 32rpx; + background: var(--button-bg); + border-top: 1rpx solid var(--border-color); +} + +.info-item { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16rpx; +} + +.info-item:last-child { + margin-bottom: 0; +} + +.info-label { + font-size: 28rpx; + color: var(--text-secondary); +} + +.info-value { + font-size: 28rpx; + color: var(--text-primary); + font-weight: 500; +} + +/* 🎨 表情选择器弹窗 */ +.emoji-picker-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + background: var(--menu-bg); + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.3s ease-out; +} + +.emoji-picker-content { + width: 90%; + max-width: 600rpx; + height: 80%; + max-height: 800rpx; + background: var(--content-bg); + border-radius: var(--radius); + box-shadow: var(--shadow); + display: flex; + flex-direction: column; + overflow: hidden; + animation: scaleIn 0.3s ease-out; +} + +@keyframes scaleIn { + from { + transform: scale(0.8); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +.emoji-picker-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32rpx; + border-bottom: 1rpx solid var(--border-color); +} + +.picker-title { + font-size: 36rpx; + font-weight: 600; + color: var(--text-primary); +} + +.close-btn { + width: 64rpx; + height: 64rpx; + border-radius: 32rpx; + background: var(--button-bg); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.close-btn:active { + background: var(--button-active); + transform: scale(0.9); +} + +.close-icon { + font-size: 28rpx; + color: var(--text-secondary); +} + +/* 🎨 表情分类标签 */ +.emoji-categories { + display: flex; + border-bottom: 1rpx solid var(--border-color); +} + +.category-tab { + flex: 1; + padding: 24rpx 16rpx; + text-align: center; + transition: all 0.2s ease; + border-bottom: 4rpx solid transparent; +} + +.category-tab.active { + border-bottom-color: #007AFF; +} + +.category-tab:active { + background: var(--button-bg); +} + +.tab-text { + font-size: 28rpx; + color: var(--text-secondary); + font-weight: 500; +} + +.category-tab.active .tab-text { + color: #007AFF; + font-weight: 600; +} + +/* 🎨 表情网格 */ +.emoji-grid-container { + flex: 1; + padding: 16rpx; +} + +.emoji-grid { + display: flex; + flex-wrap: wrap; + gap: 8rpx; +} + +.emoji-grid-item { + width: 88rpx; + height: 88rpx; + border-radius: 16rpx; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.emoji-grid-item:active { + background: var(--button-bg); + transform: scale(0.9); +} + +.grid-emoji { + font-size: 48rpx; +} + +/* 📱 响应式设计 */ +@media screen and (max-width: 375px) { + .reactions-section { + padding: 24rpx; + } + + .reaction-item { + width: 72rpx; + height: 72rpx; + border-radius: 36rpx; + } + + .reaction-emoji { + font-size: 40rpx; + } + + .action-item { + padding: 20rpx 24rpx; + } + + .action-icon { + width: 64rpx; + height: 64rpx; + border-radius: 32rpx; + margin-right: 20rpx; + } + + .icon-text { + font-size: 28rpx; + } + + .action-text { + font-size: 28rpx; + } + + .emoji-grid-item { + width: 72rpx; + height: 72rpx; + } + + .grid-emoji { + font-size: 40rpx; + } +} + +@media screen and (min-width: 414px) { + .reactions-section { + padding: 40rpx; + } + + .reaction-item { + width: 96rpx; + height: 96rpx; + border-radius: 48rpx; + } + + .reaction-emoji { + font-size: 52rpx; + } + + .action-item { + padding: 28rpx 40rpx; + } + + .action-icon { + width: 80rpx; + height: 80rpx; + border-radius: 40rpx; + margin-right: 28rpx; + } + + .icon-text { + font-size: 36rpx; + } + + .action-text { + font-size: 36rpx; + } + + .emoji-grid-item { + width: 96rpx; + height: 96rpx; + } + + .grid-emoji { + font-size: 52rpx; + } +} diff --git a/components/navigation-bar/navigation-bar.js b/components/navigation-bar/navigation-bar.js new file mode 100644 index 0000000..48d9e9f --- /dev/null +++ b/components/navigation-bar/navigation-bar.js @@ -0,0 +1,118 @@ +Component({ + options: { + multipleSlots: true // 在组件定义时的选项中启用多slot支持 + }, + /** + * 组件的属性列表 + */ + properties: { + extClass: { + type: String, + value: '' + }, + title: { + type: String, + value: '' + }, + background: { + type: String, + value: '' + }, + color: { + type: String, + value: '' + }, + back: { + type: Boolean, + value: true + }, + loading: { + type: Boolean, + value: false + }, + homeButton: { + type: Boolean, + value: false, + }, + animated: { + // 显示隐藏的时候opacity动画效果 + type: Boolean, + value: true + }, + show: { + // 显示隐藏导航,隐藏的时候navigation-bar的高度占位还在 + type: Boolean, + value: true, + observer: '_showChange' + }, + // back为true的时候,返回的页面深度 + delta: { + type: Number, + value: 1 + }, + }, + /** + * 组件的初始数据 + */ + data: { + displayStyle: '' + }, + lifetimes: { + attached() { + const rect = wx.getMenuButtonBoundingClientRect() + + // 使用新的API,提供兜底方案 + let platform, windowWidth, safeArea; + try { + const deviceInfo = wx.getDeviceInfo(); + platform = deviceInfo.platform; + const windowInfo = wx.getWindowInfo(); + windowWidth = windowInfo.windowWidth; + safeArea = windowInfo.safeArea || {}; + } catch (error) { + console.warn('使用新API失败,回退到旧API:', error); + const systemInfo = wx.getSystemInfoSync(); + platform = systemInfo.platform; + windowWidth = systemInfo.windowWidth; + safeArea = systemInfo.safeArea || {}; + } + + const isAndroid = platform === 'android' + const isDevtools = platform === 'devtools' + const { top = 0, bottom = 0 } = safeArea + this.setData({ + ios: !isAndroid, + innerPaddingRight: `padding-right: ${windowWidth - rect.left}px`, + leftWidth: `width: ${windowWidth - rect.left}px`, + safeAreaTop: isDevtools || isAndroid ? `height: calc(var(--height) + ${top}px); padding-top: ${top}px` : `` + }) + }, + }, + /** + * 组件的方法列表 + */ + methods: { + _showChange(show) { + const animated = this.data.animated + let displayStyle = '' + if (animated) { + displayStyle = `opacity: ${show ? '1' : '0' + };transition:opacity 0.5s;` + } else { + displayStyle = `display: ${show ? '' : 'none'}` + } + this.setData({ + displayStyle + }) + }, + back() { + const data = this.data + if (data.delta) { + wx.navigateBack({ + delta: data.delta + }) + } + this.triggerEvent('back', { delta: data.delta }, {}) + } + }, +}) diff --git a/components/navigation-bar/navigation-bar.json b/components/navigation-bar/navigation-bar.json new file mode 100644 index 0000000..4a20f17 --- /dev/null +++ b/components/navigation-bar/navigation-bar.json @@ -0,0 +1,5 @@ +{ + "component": true, + "styleIsolation": "apply-shared", + "usingComponents": {} +} \ No newline at end of file diff --git a/components/navigation-bar/navigation-bar.wxml b/components/navigation-bar/navigation-bar.wxml new file mode 100644 index 0000000..be9a663 --- /dev/null +++ b/components/navigation-bar/navigation-bar.wxml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{title}} + + + + + + + + + + + + diff --git a/components/navigation-bar/navigation-bar.wxss b/components/navigation-bar/navigation-bar.wxss new file mode 100644 index 0000000..8bd379e --- /dev/null +++ b/components/navigation-bar/navigation-bar.wxss @@ -0,0 +1,96 @@ +.weui-navigation-bar { + --weui-FG-0:rgba(0,0,0,.9); + --height: 44px; + --left: 16px; +} +.weui-navigation-bar .android { + --height: 48px; +} + +.weui-navigation-bar { + overflow: hidden; + color: var(--weui-FG-0); + flex: none; +} + +.weui-navigation-bar__inner { + position: relative; + top: 0; + left: 0; + height: calc(var(--height) + env(safe-area-inset-top)); + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding-top: env(safe-area-inset-top); + width: 100%; + box-sizing: border-box; +} + +.weui-navigation-bar__left { + position: relative; + padding-left: var(--left); + display: flex; + flex-direction: row; + align-items: flex-start; + height: 100%; + box-sizing: border-box; +} + +.weui-navigation-bar__btn_goback_wrapper { + padding: 11px 18px 11px 16px; + margin: -11px -18px -11px -16px; +} + +.weui-navigation-bar__btn_goback_wrapper.weui-active { + opacity: 0.5; +} + +.weui-navigation-bar__btn_goback { + font-size: 12px; + width: 12px; + height: 24px; + -webkit-mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%; + mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%; + -webkit-mask-size: cover; + mask-size: cover; + background-color: var(--weui-FG-0); +} + +.weui-navigation-bar__center { + font-size: 17px; + text-align: center; + position: relative; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + font-weight: bold; + flex: 1; + height: 100%; +} + +.weui-navigation-bar__loading { + margin-right: 4px; + align-items: center; +} + +.weui-loading { + font-size: 16px; + width: 16px; + height: 16px; + display: block; + background: transparent url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='80px' height='80px' viewBox='0 0 80 80' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3Eloading%3C/title%3E%3Cdefs%3E%3ClinearGradient x1='94.0869141%25' y1='0%25' x2='94.0869141%25' y2='90.559082%25' id='linearGradient-1'%3E%3Cstop stop-color='%23606060' stop-opacity='0' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3ClinearGradient x1='100%25' y1='8.67370605%25' x2='100%25' y2='90.6286621%25' id='linearGradient-2'%3E%3Cstop stop-color='%23606060' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3C/defs%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' opacity='0.9'%3E%3Cg%3E%3Cpath d='M40,0 C62.09139,0 80,17.90861 80,40 C80,62.09139 62.09139,80 40,80 L40,73 C58.2253967,73 73,58.2253967 73,40 C73,21.7746033 58.2253967,7 40,7 L40,0 Z' fill='url(%23linearGradient-1)'%3E%3C/path%3E%3Cpath d='M40,0 L40,7 C21.7746033,7 7,21.7746033 7,40 C7,58.2253967 21.7746033,73 40,73 L40,80 C17.90861,80 0,62.09139 0,40 C0,17.90861 17.90861,0 40,0 Z' fill='url(%23linearGradient-2)'%3E%3C/path%3E%3Ccircle id='Oval' fill='%23606060' cx='40.5' cy='3.5' r='3.5'%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A") no-repeat; + background-size: 100%; + margin-left: 0; + animation: loading linear infinite 1s; +} + +@keyframes loading { + from { + transform: rotate(0); + } + to { + transform: rotate(360deg); + } +} diff --git a/components/page-transition/page-transition.js b/components/page-transition/page-transition.js new file mode 100644 index 0000000..cd5a302 --- /dev/null +++ b/components/page-transition/page-transition.js @@ -0,0 +1,456 @@ +// 🎬 页面过渡组件逻辑 +const animationManager = require('../../utils/animation-manager.js'); + +Component({ + properties: { + // 过渡类型 + transitionType: { + type: String, + value: 'fade', + observer: 'onTransitionTypeChange' + }, + + // 是否显示 + visible: { + type: Boolean, + value: true, + observer: 'onVisibleChange' + }, + + // 动画时长 + duration: { + type: Number, + value: 300 + }, + + // 缓动函数 + easing: { + type: String, + value: 'ease-out' + }, + + // 是否显示遮罩 + showOverlay: { + type: Boolean, + value: false + }, + + // 遮罩类型 + overlayType: { + type: String, + value: 'fade-black' + }, + + // 是否显示加载 + showLoading: { + type: Boolean, + value: false + }, + + // 加载文本 + loadingText: { + type: String, + value: '加载中...' + }, + + // 是否启用硬件加速 + hardwareAccelerated: { + type: Boolean, + value: true + }, + + // 调试模式 + debug: { + type: Boolean, + value: false + } + }, + + data: { + // 过渡状态 + transitionState: 'idle', // idle, entering, entered, leaving, left + + // 动画数据 + animationData: null, + loadingAnimation: null, + + // 样式 + animationStyle: '', + overlayStyle: '', + + // CSS类 + transitionClass: '', + overlayClass: '' + }, + + lifetimes: { + attached() { + console.log('🎬 页面过渡组件加载'); + this.initComponent(); + }, + + ready() { + console.log('🎬 页面过渡组件就绪'); + this.setupInitialState(); + }, + + detached() { + console.log('🎬 页面过渡组件卸载'); + this.cleanup(); + } + }, + + methods: { + // 初始化组件 + initComponent() { + // 设置初始过渡类 + this.updateTransitionClass(); + + // 设置遮罩类 + this.updateOverlayClass(); + + // 创建加载动画 + this.createLoadingAnimation(); + }, + + // 设置初始状态 + setupInitialState() { + if (this.properties.visible) { + this.enter(); + } else { + this.setData({ + transitionState: 'left' + }); + } + }, + + // 过渡类型变化处理 + onTransitionTypeChange(newType, oldType) { + if (newType !== oldType) { + this.updateTransitionClass(); + } + }, + + // 可见性变化处理 + onVisibleChange(visible, wasVisible) { + if (visible === wasVisible) return; + + if (visible) { + this.enter(); + } else { + this.leave(); + } + }, + + // 🎭 ===== 过渡控制 ===== + + // 进入动画 + async enter() { + if (this.data.transitionState === 'entering' || this.data.transitionState === 'entered') { + return; + } + + console.log('🎬 开始进入动画:', this.properties.transitionType); + + this.setData({ + transitionState: 'entering' + }); + + this.triggerEvent('transitionstart', { + type: 'enter', + transitionType: this.properties.transitionType + }); + + try { + // 创建进入动画 + const animation = this.createEnterAnimation(); + + this.setData({ + animationData: animation.export() + }); + + // 等待动画完成 + await this.waitForAnimation(); + + this.setData({ + transitionState: 'entered' + }); + + this.triggerEvent('transitionend', { + type: 'enter', + transitionType: this.properties.transitionType + }); + + console.log('✅ 进入动画完成'); + + } catch (error) { + console.error('❌ 进入动画失败:', error); + this.setData({ + transitionState: 'entered' + }); + } + }, + + // 退出动画 + async leave() { + if (this.data.transitionState === 'leaving' || this.data.transitionState === 'left') { + return; + } + + console.log('🎬 开始退出动画:', this.properties.transitionType); + + this.setData({ + transitionState: 'leaving' + }); + + this.triggerEvent('transitionstart', { + type: 'leave', + transitionType: this.properties.transitionType + }); + + try { + // 创建退出动画 + const animation = this.createLeaveAnimation(); + + this.setData({ + animationData: animation.export() + }); + + // 等待动画完成 + await this.waitForAnimation(); + + this.setData({ + transitionState: 'left' + }); + + this.triggerEvent('transitionend', { + type: 'leave', + transitionType: this.properties.transitionType + }); + + console.log('✅ 退出动画完成'); + + } catch (error) { + console.error('❌ 退出动画失败:', error); + this.setData({ + transitionState: 'left' + }); + } + }, + + // 🎨 ===== 动画创建 ===== + + // 创建进入动画 + createEnterAnimation() { + const transitionType = this.properties.transitionType; + + switch (transitionType) { + case 'slideLeft': + return animationManager.slideIn('left', { + duration: this.properties.duration, + timingFunction: this.properties.easing + }); + + case 'slideRight': + return animationManager.slideIn('right', { + duration: this.properties.duration, + timingFunction: this.properties.easing + }); + + case 'slideUp': + return animationManager.slideIn('up', { + duration: this.properties.duration, + timingFunction: this.properties.easing + }); + + case 'slideDown': + return animationManager.slideIn('down', { + duration: this.properties.duration, + timingFunction: this.properties.easing + }); + + case 'scale': + return animationManager.scale(1, { + duration: this.properties.duration, + timingFunction: this.properties.easing + }); + + case 'bounce': + return animationManager.bounceIn({ + duration: this.properties.duration + }); + + case 'fade': + default: + return animationManager.fadeIn({ + duration: this.properties.duration, + timingFunction: this.properties.easing + }); + } + }, + + // 创建退出动画 + createLeaveAnimation() { + const transitionType = this.properties.transitionType; + + switch (transitionType) { + case 'slideLeft': + return animationManager.slideOut('left', '100%', { + duration: this.properties.duration, + timingFunction: this.properties.easing + }); + + case 'slideRight': + return animationManager.slideOut('right', '100%', { + duration: this.properties.duration, + timingFunction: this.properties.easing + }); + + case 'slideUp': + return animationManager.slideOut('up', '100%', { + duration: this.properties.duration, + timingFunction: this.properties.easing + }); + + case 'slideDown': + return animationManager.slideOut('down', '100%', { + duration: this.properties.duration, + timingFunction: this.properties.easing + }); + + case 'scale': + return animationManager.scale(0, { + duration: this.properties.duration, + timingFunction: this.properties.easing + }); + + case 'bounce': + return animationManager.bounceOut({ + duration: this.properties.duration + }); + + case 'fade': + default: + return animationManager.fadeOut({ + duration: this.properties.duration, + timingFunction: this.properties.easing + }); + } + }, + + // 创建加载动画 + createLoadingAnimation() { + const loadingAnimation = animationManager.loadingSpinner({ + duration: 1000 + }); + + this.setData({ + loadingAnimation: loadingAnimation.export() + }); + + // 循环播放加载动画 + if (this.properties.showLoading) { + this.startLoadingLoop(); + } + }, + + // 开始加载循环 + startLoadingLoop() { + this.loadingTimer = setInterval(() => { + if (this.properties.showLoading) { + const loadingAnimation = animationManager.loadingSpinner({ + duration: 1000 + }); + + this.setData({ + loadingAnimation: loadingAnimation.export() + }); + } else { + this.stopLoadingLoop(); + } + }, 1000); + }, + + // 停止加载循环 + stopLoadingLoop() { + if (this.loadingTimer) { + clearInterval(this.loadingTimer); + this.loadingTimer = null; + } + }, + + // 🎯 ===== 样式管理 ===== + + // 更新过渡类 + updateTransitionClass() { + let transitionClass = this.properties.transitionType; + + if (this.properties.hardwareAccelerated) { + transitionClass += ' hardware-accelerated'; + } + + if (this.properties.debug) { + transitionClass += ' debug'; + } + + this.setData({ + transitionClass: transitionClass + }); + }, + + // 更新遮罩类 + updateOverlayClass() { + this.setData({ + overlayClass: this.properties.overlayType + }); + }, + + // 🔧 ===== 工具方法 ===== + + // 等待动画完成 + waitForAnimation() { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, this.properties.duration + 50); // 添加50ms缓冲 + }); + }, + + // 手动触发进入 + triggerEnter() { + this.enter(); + }, + + // 手动触发退出 + triggerLeave() { + this.leave(); + }, + + // 重置状态 + reset() { + this.setData({ + transitionState: 'idle', + animationData: null + }); + }, + + // 获取当前状态 + getState() { + return { + transitionState: this.data.transitionState, + transitionType: this.properties.transitionType, + visible: this.properties.visible + }; + }, + + // 清理资源 + cleanup() { + this.stopLoadingLoop(); + + if (this.animationTimer) { + clearTimeout(this.animationTimer); + this.animationTimer = null; + } + } + } +}); diff --git a/components/page-transition/page-transition.json b/components/page-transition/page-transition.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/components/page-transition/page-transition.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/components/page-transition/page-transition.wxml b/components/page-transition/page-transition.wxml new file mode 100644 index 0000000..0e7b40e --- /dev/null +++ b/components/page-transition/page-transition.wxml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + {{loadingText}} + + diff --git a/components/page-transition/page-transition.wxss b/components/page-transition/page-transition.wxss new file mode 100644 index 0000000..ae28115 --- /dev/null +++ b/components/page-transition/page-transition.wxss @@ -0,0 +1,386 @@ +/* 🎬 页面过渡组件样式 */ + +/* CSS变量定义 */ +.page-transition-container { + --primary-color: #007AFF; + --background-color: #F2F2F7; + --surface-color: #FFFFFF; + --text-primary: #000000; + --text-secondary: #8E8E93; + --shadow-medium: 0 8rpx 24rpx rgba(0, 0, 0, 0.15); + --radius-large: 20rpx; +} + +/* 🌙 深色模式支持 */ +@media (prefers-color-scheme: dark) { + .page-transition-container { + --primary-color: #0A84FF; + --background-color: #000000; + --surface-color: #1C1C1E; + --text-primary: #FFFFFF; + --text-secondary: #8E8E93; + --shadow-medium: 0 8rpx 24rpx rgba(0, 0, 0, 0.4); + } +} + +.page-transition-container { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; +} + +/* 🎭 过渡类型样式 */ +.page-transition-container.slide-left { + transform: translateX(100%); +} + +.page-transition-container.slide-right { + transform: translateX(-100%); +} + +.page-transition-container.slide-up { + transform: translateY(100%); +} + +.page-transition-container.slide-down { + transform: translateY(-100%); +} + +.page-transition-container.fade { + opacity: 0; +} + +.page-transition-container.scale { + transform: scale(0.8); + opacity: 0; +} + +.page-transition-container.flip { + transform: rotateY(90deg); +} + +.page-transition-container.zoom { + transform: scale(1.2); + opacity: 0; +} + +/* 激活状态 */ +.page-transition-container.active { + transform: translateX(0) translateY(0) scale(1) rotateY(0); + opacity: 1; +} + +/* 🎨 过渡遮罩 */ +.transition-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; + pointer-events: none; +} + +.transition-overlay.fade-black { + background: rgba(0, 0, 0, 0.5); +} + +.transition-overlay.fade-white { + background: rgba(255, 255, 255, 0.8); +} + +.transition-overlay.blur { + backdrop-filter: blur(10rpx); + background: rgba(255, 255, 255, 0.1); +} + +.transition-overlay.gradient { + background: linear-gradient(45deg, + rgba(0, 122, 255, 0.1) 0%, + rgba(90, 200, 250, 0.1) 100%); +} + +/* 🔄 加载指示器 */ +.transition-loading { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1001; + display: flex; + flex-direction: column; + align-items: center; + gap: 24rpx; +} + +.loading-spinner { + width: 60rpx; + height: 60rpx; + border: 4rpx solid rgba(0, 122, 255, 0.2); + border-top: 4rpx solid var(--primary-color); + border-radius: 50%; +} + +.loading-text { + font-size: 28rpx; + color: var(--text-secondary); + text-align: center; +} + +/* 🎪 预定义动画类 */ +@keyframes slideInLeft { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slideInRight { + from { + transform: translateX(-100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slideInUp { + from { + transform: translateY(100%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes slideInDown { + from { + transform: translateY(-100%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes scaleIn { + from { + transform: scale(0.8); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +@keyframes flipIn { + from { + transform: rotateY(90deg); + opacity: 0; + } + to { + transform: rotateY(0); + opacity: 1; + } +} + +@keyframes zoomIn { + from { + transform: scale(1.2); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +@keyframes bounceIn { + 0% { + transform: scale(0.3); + opacity: 0; + } + 50% { + transform: scale(1.05); + opacity: 1; + } + 70% { + transform: scale(0.95); + } + 100% { + transform: scale(1); + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* 🎭 动画类应用 */ +.page-transition-container.animate-slide-left { + animation: slideInLeft 0.4s ease-out; +} + +.page-transition-container.animate-slide-right { + animation: slideInRight 0.4s ease-out; +} + +.page-transition-container.animate-slide-up { + animation: slideInUp 0.4s ease-out; +} + +.page-transition-container.animate-slide-down { + animation: slideInDown 0.4s ease-out; +} + +.page-transition-container.animate-fade { + animation: fadeIn 0.3s ease-out; +} + +.page-transition-container.animate-scale { + animation: scaleIn 0.3s ease-out; +} + +.page-transition-container.animate-flip { + animation: flipIn 0.5s ease-out; +} + +.page-transition-container.animate-zoom { + animation: zoomIn 0.3s ease-out; +} + +.page-transition-container.animate-bounce { + animation: bounceIn 0.6s ease-out; +} + +.loading-spinner { + animation: spin 1s linear infinite; +} + +/* 📱 响应式设计 */ +@media screen and (max-width: 375px) { + .loading-spinner { + width: 50rpx; + height: 50rpx; + border-width: 3rpx; + } + + .loading-text { + font-size: 24rpx; + } +} + +@media screen and (min-width: 414px) { + .loading-spinner { + width: 70rpx; + height: 70rpx; + border-width: 5rpx; + } + + .loading-text { + font-size: 32rpx; + } +} + +/* 🎨 性能优化 */ +.page-transition-container { + will-change: transform, opacity; + backface-visibility: hidden; + -webkit-backface-visibility: hidden; +} + +/* 减少重绘 */ +.page-transition-container .transition-content { + transform-style: preserve-3d; +} + +/* 硬件加速 */ +.page-transition-container.hardware-accelerated { + transform: translateZ(0); + -webkit-transform: translateZ(0); +} + +/* 🎭 特殊效果 */ +.page-transition-container.glass-effect { + backdrop-filter: blur(20rpx); + background: rgba(255, 255, 255, 0.1); + border: 1rpx solid rgba(255, 255, 255, 0.2); +} + +.page-transition-container.shadow-effect { + box-shadow: var(--shadow-medium); +} + +.page-transition-container.glow-effect { + box-shadow: 0 0 40rpx rgba(0, 122, 255, 0.3); +} + +/* 🎪 组合动画 */ +.page-transition-container.slide-fade { + animation: slideInLeft 0.4s ease-out, fadeIn 0.4s ease-out; +} + +.page-transition-container.scale-fade { + animation: scaleIn 0.3s ease-out, fadeIn 0.3s ease-out; +} + +.page-transition-container.flip-fade { + animation: flipIn 0.5s ease-out, fadeIn 0.5s ease-out; +} + +/* 🎯 状态指示器 */ +.page-transition-container.loading { + pointer-events: none; +} + +.page-transition-container.transitioning { + overflow: hidden; +} + +.page-transition-container.completed { + transform: none; + opacity: 1; +} + +/* 🔧 调试模式 */ +.page-transition-container.debug { + border: 2rpx dashed #ff0000; + position: relative; +} + +.page-transition-container.debug::before { + content: 'TRANSITION DEBUG'; + position: absolute; + top: 10rpx; + left: 10rpx; + background: #ff0000; + color: white; + padding: 4rpx 8rpx; + font-size: 20rpx; + z-index: 9999; +} diff --git a/components/voice-message/voice-message.js b/components/voice-message/voice-message.js new file mode 100644 index 0000000..1ac730e --- /dev/null +++ b/components/voice-message/voice-message.js @@ -0,0 +1,321 @@ +// 🎤 语音消息组件逻辑 +const voiceMessageManager = require('../../utils/voice-message-manager.js'); + +Component({ + properties: { + // 语音消息数据 + voiceData: { + type: Object, + value: {}, + observer: 'onVoiceDataChange' + }, + + // 是否为自己发送的消息 + isSelf: { + type: Boolean, + value: false + }, + + // 消息ID + messageId: { + type: String, + value: '' + } + }, + + data: { + // 播放状态 + isPlaying: false, + isLoading: false, + hasError: false, + + // 播放进度 + currentTime: 0, + duration: 0, + playProgress: 0, + + // 波形数据 + waveformData: [], + currentWaveIndex: 0, + + // 语音信息 + voiceUrl: '', + voiceDuration: 0 + }, + + lifetimes: { + attached() { + console.log('🎤 语音消息组件加载'); + this.initComponent(); + }, + + detached() { + console.log('🎤 语音消息组件卸载'); + this.cleanup(); + } + }, + + methods: { + // 初始化组件 + initComponent() { + // 注册语音管理器事件 + this.registerVoiceEvents(); + + // 生成波形数据 + this.generateWaveform(); + + // 检查当前播放状态 + this.checkPlayingState(); + }, + + // 语音数据变化处理 + onVoiceDataChange(newData, oldData) { + if (!newData || JSON.stringify(newData) === JSON.stringify(oldData)) { + return; + } + + console.log('🎤 语音数据更新:', newData); + + this.setData({ + voiceUrl: newData.url || '', + voiceDuration: newData.duration || 0, + duration: newData.duration || 0 + }); + + // 重新生成波形 + this.generateWaveform(); + + // 检查播放状态 + this.checkPlayingState(); + }, + + // 注册语音管理器事件 + registerVoiceEvents() { + // 播放开始事件 + voiceMessageManager.on('playStart', () => { + this.checkPlayingState(); + }); + + // 播放结束事件 + voiceMessageManager.on('playEnd', () => { + this.setData({ + isPlaying: false, + currentTime: 0, + playProgress: 0, + currentWaveIndex: 0 + }); + }); + + // 播放进度更新事件 + voiceMessageManager.on('playTimeUpdate', (data) => { + if (this.isCurrentMessage()) { + this.updatePlayProgress(data.currentTime, data.duration); + } + }); + + // 播放错误事件 + voiceMessageManager.on('playError', (error) => { + if (this.isCurrentMessage()) { + console.error('🎤 语音播放错误:', error); + this.setData({ + isPlaying: false, + isLoading: false, + hasError: true + }); + } + }); + + // 播放可以开始事件 + voiceMessageManager.on('playCanplay', () => { + if (this.isCurrentMessage()) { + this.setData({ + isLoading: false, + hasError: false + }); + } + }); + }, + + // 检查是否为当前播放的消息 + isCurrentMessage() { + const currentMessageId = voiceMessageManager.getCurrentPlayingMessageId(); + return currentMessageId === this.properties.messageId; + }, + + // 检查播放状态 + checkPlayingState() { + const isCurrentlyPlaying = this.isCurrentMessage() && voiceMessageManager.isPlaying(); + + this.setData({ + isPlaying: isCurrentlyPlaying + }); + }, + + // 切换播放状态 + async togglePlay() { + if (this.data.hasError) { + return this.retryPlay(); + } + + if (this.data.isLoading) { + return; + } + + try { + if (this.data.isPlaying) { + // 暂停播放 + voiceMessageManager.pausePlaying(); + this.setData({ isPlaying: false }); + } else { + // 开始播放 + await this.startPlay(); + } + } catch (error) { + console.error('🎤 切换播放状态失败:', error); + this.setData({ + hasError: true, + isLoading: false, + isPlaying: false + }); + } + }, + + // 开始播放 + async startPlay() { + if (!this.data.voiceUrl) { + console.error('🎤 语音URL为空'); + return; + } + + try { + this.setData({ + isLoading: true, + hasError: false + }); + + // 播放语音消息 + await voiceMessageManager.playVoiceMessage( + this.data.voiceUrl, + this.properties.messageId + ); + + this.setData({ + isPlaying: true, + isLoading: false + }); + + console.log('🎤 开始播放语音消息'); + + } catch (error) { + console.error('🎤 播放语音消息失败:', error); + this.setData({ + hasError: true, + isLoading: false, + isPlaying: false + }); + } + }, + + // 重试播放 + async retryPlay() { + console.log('🎤 重试播放语音消息'); + + this.setData({ + hasError: false, + isLoading: false, + isPlaying: false + }); + + await this.startPlay(); + }, + + // 更新播放进度 + updatePlayProgress(currentTime, duration) { + if (!duration || duration <= 0) return; + + const progress = (currentTime / duration) * 100; + const waveIndex = Math.floor((currentTime / duration) * this.data.waveformData.length); + + this.setData({ + currentTime: currentTime, + duration: duration, + playProgress: progress, + currentWaveIndex: Math.max(0, waveIndex) + }); + }, + + // 生成波形数据 + generateWaveform() { + const duration = this.data.voiceDuration || this.data.duration || 1000; + const barCount = Math.min(Math.max(Math.floor(duration / 200), 8), 30); // 8-30个波形条 + + const waveformData = []; + for (let i = 0; i < barCount; i++) { + // 生成随机高度,模拟真实波形 + const height = Math.random() * 60 + 20; // 20-80%的高度 + waveformData.push(height); + } + + this.setData({ + waveformData: waveformData, + currentWaveIndex: 0 + }); + + console.log('🌊 生成波形数据:', waveformData.length, '个波形条'); + }, + + // 格式化时长显示 + formatDuration(duration) { + if (!duration || duration <= 0) return '0"'; + + const seconds = Math.floor(duration / 1000); + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + + if (minutes > 0) { + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; + } else { + return `${remainingSeconds}"`; + } + }, + + // 格式化时间显示 + formatTime(time) { + if (!time || time <= 0) return '0:00'; + + const totalSeconds = Math.floor(time); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + + return `${minutes}:${seconds.toString().padStart(2, '0')}`; + }, + + // 获取语音文件大小描述 + getFileSizeDescription(fileSize) { + if (!fileSize || fileSize <= 0) return ''; + + if (fileSize < 1024) { + return `${fileSize}B`; + } else if (fileSize < 1024 * 1024) { + return `${(fileSize / 1024).toFixed(1)}KB`; + } else { + return `${(fileSize / (1024 * 1024)).toFixed(1)}MB`; + } + }, + + // 清理资源 + cleanup() { + // 如果当前正在播放这个消息,停止播放 + if (this.isCurrentMessage() && voiceMessageManager.isPlaying()) { + voiceMessageManager.stopPlaying(); + } + + // 移除事件监听器 + voiceMessageManager.off('playStart'); + voiceMessageManager.off('playEnd'); + voiceMessageManager.off('playTimeUpdate'); + voiceMessageManager.off('playError'); + voiceMessageManager.off('playCanplay'); + } + } +}); diff --git a/components/voice-message/voice-message.json b/components/voice-message/voice-message.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/components/voice-message/voice-message.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/components/voice-message/voice-message.wxml b/components/voice-message/voice-message.wxml new file mode 100644 index 0000000..794a874 --- /dev/null +++ b/components/voice-message/voice-message.wxml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + {{formatDuration(duration)}} + + + + + + + + + + {{formatTime(currentTime)}} + {{formatTime(duration)}} + + + + + + + 加载中... + + + + + ⚠️ + 播放失败 + 重试 + + diff --git a/components/voice-message/voice-message.wxss b/components/voice-message/voice-message.wxss new file mode 100644 index 0000000..bf52be7 --- /dev/null +++ b/components/voice-message/voice-message.wxss @@ -0,0 +1,443 @@ +/* 🎤 语音消息组件样式 */ + +/* CSS变量定义 */ +.voice-message-container { + --primary-color: #007AFF; + --primary-light: #5AC8FA; + --success-color: #34C759; + --warning-color: #FF9500; + --danger-color: #FF3B30; + --background-light: #F2F2F7; + --background-dark: #1C1C1E; + --text-primary: #000000; + --text-secondary: #8E8E93; + --border-color: #E5E5EA; + --shadow-light: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); + --radius-medium: 12rpx; + --radius-large: 20rpx; +} + +/* 🌙 深色模式支持 */ +@media (prefers-color-scheme: dark) { + .voice-message-container { + --primary-color: #0A84FF; + --background-light: #2C2C2E; + --background-dark: #1C1C1E; + --text-primary: #FFFFFF; + --text-secondary: #8E8E93; + --border-color: #38383A; + --shadow-light: 0 2rpx 8rpx rgba(0, 0, 0, 0.3); + } +} + +.voice-message-container { + max-width: 480rpx; + margin: 8rpx 0; + position: relative; +} + +/* 🎨 语音气泡 */ +.voice-bubble { + display: flex; + align-items: center; + padding: 24rpx; + border-radius: var(--radius-large); + box-shadow: var(--shadow-light); + transition: all 0.3s ease; + min-width: 200rpx; + position: relative; + overflow: hidden; +} + +/* 自己发送的消息 */ +.voice-message-container.self .voice-bubble { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%); + color: white; +} + +/* 他人发送的消息 */ +.voice-message-container.other .voice-bubble { + background: var(--background-light); + color: var(--text-primary); + border: 1rpx solid var(--border-color); +} + +/* 播放状态 */ +.voice-message-container.playing .voice-bubble { + transform: scale(1.02); +} + +.voice-message-container.self.playing .voice-bubble { + background: linear-gradient(135deg, var(--success-color) 0%, var(--primary-light) 100%); +} + +.voice-message-container.other.playing .voice-bubble { + background: rgba(0, 122, 255, 0.1); + border-color: var(--primary-color); +} + +/* 🎵 播放按钮 */ +.play-button { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 24rpx; + transition: all 0.3s ease; + position: relative; + box-sizing: border-box; + overflow: hidden; + flex-shrink: 0; /* 不允许在flex布局中被压缩 */ + flex: 0 0 auto; /* 宽高由自身决定 */ + min-width: 80rpx; /* 保底宽度,维持正圆 */ +} + +.voice-message-container.self .play-button { + background: rgba(255, 255, 255, 0.2); +} + +.voice-message-container.other .play-button { + background: var(--primary-color); +} + +.play-button:active { + transform: scale(0.95); +} + +.play-icon { + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.play-icon .icon { + font-size: 32rpx; + font-weight: bold; +} + +.voice-message-container.self .play-icon .icon { + color: white; +} + +.voice-message-container.other .play-icon .icon { + color: white; +} + +/* 播放动画 */ +.play-icon.play .icon { + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } +} + +/* 🌊 语音波形 */ +.voice-waveform { + flex: 1; + margin-right: 24rpx; + height: 60rpx; + display: flex; + align-items: center; +} + +.waveform-container { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + height: 100%; + gap: 4rpx; +} + +.wave-bar { + width: 6rpx; + border-radius: 3rpx; + transition: all 0.3s ease; + min-height: 8rpx; +} + +.voice-message-container.self .wave-bar { + background: rgba(255, 255, 255, 0.4); +} + +.voice-message-container.other .wave-bar { + background: var(--border-color); +} + +.voice-message-container.self .wave-bar.active { + background: white; + transform: scaleY(1.2); +} + +.voice-message-container.other .wave-bar.active { + background: var(--primary-color); + transform: scaleY(1.2); +} + +/* 波形动画 */ +.voice-message-container.playing .wave-bar.active { + animation: waveAnimation 1.5s ease-in-out infinite; +} + +@keyframes waveAnimation { + 0%, 100% { transform: scaleY(1); } + 50% { transform: scaleY(1.5); } +} + +/* ⏱️ 语音时长 */ +.voice-duration { + min-width: 60rpx; + text-align: right; +} + +.duration-text { + font-size: 24rpx; + font-weight: 500; +} + +.voice-message-container.self .duration-text { + color: rgba(255, 255, 255, 0.9); +} + +.voice-message-container.other .duration-text { + color: var(--text-secondary); +} + +/* 📊 播放进度条 */ +.progress-container { + margin-top: 16rpx; + padding: 0 24rpx; +} + +.progress-bar { + height: 4rpx; + background: var(--border-color); + border-radius: 2rpx; + overflow: hidden; + margin-bottom: 8rpx; +} + +.progress-fill { + height: 100%; + background: var(--primary-color); + border-radius: 2rpx; + transition: width 0.1s linear; +} + +.progress-time { + display: flex; + justify-content: space-between; + align-items: center; +} + +.current-time, +.total-time { + font-size: 20rpx; + color: var(--text-secondary); +} + +/* 🔄 加载状态 */ +.loading-container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.1); + border-radius: var(--radius-large); + backdrop-filter: blur(10rpx); +} + +.loading-spinner { + width: 40rpx; + height: 40rpx; + border: 3rpx solid rgba(255, 255, 255, 0.3); + border-top: 3rpx solid white; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-right: 16rpx; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-text { + font-size: 24rpx; + color: white; +} + +/* ⚠️ 错误状态 */ +.error-container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 59, 48, 0.1); + border-radius: var(--radius-large); + backdrop-filter: blur(10rpx); + gap: 12rpx; +} + +.error-icon { + font-size: 32rpx; +} + +.error-text { + font-size: 24rpx; + color: var(--danger-color); +} + +.retry-button { + font-size: 24rpx; + color: var(--primary-color); + text-decoration: underline; + transition: all 0.2s ease; +} + +.retry-button:active { + opacity: 0.7; +} + +/* 🎨 特殊效果 */ +.voice-bubble::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%); + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; +} + +.voice-bubble:active::before { + opacity: 1; +} + +/* 📱 响应式设计 */ +@media screen and (max-width: 375px) { + .voice-message-container { + max-width: 400rpx; + } + + .voice-bubble { + padding: 20rpx; + } + + .play-button { + width: 70rpx; + height: 70rpx; + margin-right: 20rpx; + min-width: 70rpx; + } + + .play-icon .icon { + font-size: 28rpx; + } + + .voice-waveform { + height: 50rpx; + margin-right: 20rpx; + } + + .wave-bar { + width: 5rpx; + } +} + +@media screen and (min-width: 414px) { + .voice-message-container { + max-width: 520rpx; + } + + .voice-bubble { + padding: 28rpx; + } + + .play-button { + width: 90rpx; + height: 90rpx; + margin-right: 28rpx; + min-width: 90rpx; + } + + .play-icon .icon { + font-size: 36rpx; + } + + .voice-waveform { + height: 70rpx; + margin-right: 28rpx; + } + + .wave-bar { + width: 7rpx; + } +} + +/* 🎭 动画增强 */ +.voice-message-container { + animation: slideIn 0.3s ease-out; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(20rpx); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 长按效果 */ +.voice-bubble { + user-select: none; + -webkit-user-select: none; +} + +.voice-bubble:active { + transform: scale(0.98); +} + +/* 可访问性 */ +.voice-bubble[aria-pressed="true"] { + outline: 2rpx solid var(--primary-color); + outline-offset: 4rpx; +} + +/* 状态指示器 */ +.voice-message-container::after { + content: ''; + position: absolute; + top: -4rpx; + right: -4rpx; + width: 16rpx; + height: 16rpx; + border-radius: 8rpx; + opacity: 0; + transition: all 0.3s ease; +} + +.voice-message-container.playing::after { + opacity: 1; + background: var(--success-color); + animation: pulse 2s infinite; +} diff --git a/components/voice-recorder/voice-recorder.js b/components/voice-recorder/voice-recorder.js new file mode 100644 index 0000000..f4f5f21 --- /dev/null +++ b/components/voice-recorder/voice-recorder.js @@ -0,0 +1,562 @@ +// 🎤 语音录制组件逻辑 +const voiceMessageManager = require('../../utils/voice-message-manager.js'); + +Component({ + properties: { + // 是否显示录音界面 + visible: { + type: Boolean, + value: false, + observer: 'onVisibleChange' + }, + + // 最大录音时长(毫秒) + maxDuration: { + type: Number, + value: 60000 + }, + + // 最小录音时长(毫秒) + minDuration: { + type: Number, + value: 1000 + } + }, + + data: { + // 录音状态:idle, recording, paused, completed, error + recordingState: 'idle', + + // 状态文本 + statusText: '准备录音', + + // 录音时长 + recordingDuration: 0, + + // 实时波形数据 + realtimeWaveform: [], + + // 最终波形数据 + finalWaveform: [], + + // 录音文件信息 + tempFilePath: '', + fileSize: 0, + + // 播放状态 + isPlaying: false, + + // 错误信息 + errorMessage: '', + + // 权限引导 + showPermissionGuide: false, + + // 定时器 + durationTimer: null, + waveformTimer: null + }, + + lifetimes: { + attached() { + console.log('🎤 语音录制组件加载'); + this.initComponent(); + }, + + detached() { + console.log('🎤 语音录制组件卸载'); + this.cleanup(); + } + }, + + methods: { + // 事件阻断占位 + noop() {}, + // 初始化组件 + initComponent() { + // 注册语音管理器事件 + this.registerVoiceEvents(); + + // 初始化波形数据 + this.initWaveform(); + }, + + // 可见性变化处理 + onVisibleChange(visible) { + if (visible) { + this.resetRecorder(); + } else { + this.cleanup(); + } + }, + + // 注册语音管理器事件 + registerVoiceEvents() { + // 录音开始事件 + voiceMessageManager.on('recordStart', () => { + this.setData({ + recordingState: 'recording', + statusText: '正在录音...', + recordingDuration: 0 + }); + + this.startDurationTimer(); + this.startWaveformAnimation(); + }); + + // 录音停止事件 + voiceMessageManager.on('recordStop', (data) => { + this.setData({ + recordingState: 'completed', + statusText: '录音完成', + recordingDuration: data.duration, + tempFilePath: data.tempFilePath, + fileSize: data.fileSize + }); + + this.stopTimers(); + this.generateFinalWaveform(); + }); + + // 录音暂停事件 + voiceMessageManager.on('recordPause', () => { + this.setData({ + recordingState: 'paused', + statusText: '录音已暂停' + }); + + this.stopTimers(); + }); + + // 录音恢复事件 + voiceMessageManager.on('recordResume', () => { + this.setData({ + recordingState: 'recording', + statusText: '正在录音...' + }); + + this.startDurationTimer(); + this.startWaveformAnimation(); + }); + + // 录音错误事件 + voiceMessageManager.on('recordError', (error) => { + console.error('🎤 录音错误:', error); + + this.setData({ + recordingState: 'error', + statusText: '录音失败', + errorMessage: this.getErrorMessage(error) + }); + + this.stopTimers(); + }); + + // 录音帧数据事件 + voiceMessageManager.on('recordFrame', (data) => { + this.updateRealtimeWaveform(data.frameBuffer); + }); + }, + + // 🎤 ===== 录音控制 ===== + + // 开始录音 + async startRecording() { + console.log('🎤 开始录音'); + + try { + await voiceMessageManager.startRecording({ + duration: this.properties.maxDuration, + format: 'mp3' + }); + + } catch (error) { + console.error('🎤 开始录音失败:', error); + + if (error.message.includes('权限')) { + this.setData({ + showPermissionGuide: true + }); + } else { + this.setData({ + recordingState: 'error', + statusText: '录音失败', + errorMessage: this.getErrorMessage(error) + }); + } + } + }, + + // 停止录音 + stopRecording() { + console.log('🎤 停止录音'); + + try { + // 检查最小录音时长 + if (this.data.recordingDuration < this.properties.minDuration) { + wx.showToast({ + title: `录音时长不能少于${this.properties.minDuration / 1000}秒`, + icon: 'none' + }); + return; + } + + voiceMessageManager.stopRecording(); + + } catch (error) { + console.error('🎤 停止录音失败:', error); + this.setData({ + recordingState: 'error', + statusText: '停止录音失败', + errorMessage: this.getErrorMessage(error) + }); + } + }, + + // 暂停录音 + pauseRecording() { + console.log('🎤 暂停录音'); + + try { + voiceMessageManager.pauseRecording(); + } catch (error) { + console.error('🎤 暂停录音失败:', error); + } + }, + + // 恢复录音 + resumeRecording() { + console.log('🎤 恢复录音'); + + try { + voiceMessageManager.resumeRecording(); + } catch (error) { + console.error('🎤 恢复录音失败:', error); + } + }, + + // 取消录音 + cancelRecording() { + console.log('🎤 取消录音'); + + try { + voiceMessageManager.cancelRecording(); + this.resetRecorder(); + } catch (error) { + console.error('🎤 取消录音失败:', error); + } + }, + + // 丢弃录音 + discardRecording() { + console.log('🎤 丢弃录音'); + this.resetRecorder(); + }, + + // 🔊 ===== 播放控制 ===== + + // 播放预览 + async playPreview() { + if (!this.data.tempFilePath) { + return; + } + + try { + if (this.data.isPlaying) { + voiceMessageManager.stopPlaying(); + this.setData({ isPlaying: false }); + } else { + await voiceMessageManager.playVoiceMessage(this.data.tempFilePath); + this.setData({ isPlaying: true }); + + // 监听播放结束 + const onPlayEnd = () => { + this.setData({ isPlaying: false }); + voiceMessageManager.off('playEnd', onPlayEnd); + }; + voiceMessageManager.on('playEnd', onPlayEnd); + } + + } catch (error) { + console.error('🎤 播放预览失败:', error); + wx.showToast({ + title: '播放失败', + icon: 'none' + }); + } + }, + + // 📤 ===== 发送录音 ===== + + // 发送录音 + async sendRecording() { + if (!this.data.tempFilePath || !this.data.recordingDuration) { + return; + } + + console.log('📤 发送录音'); + + try { + wx.showLoading({ + title: '上传中...', + mask: true + }); + + // 上传语音文件 + const uploadResult = await voiceMessageManager.uploadVoiceFile( + this.data.tempFilePath, + this.data.recordingDuration + ); + + wx.hideLoading(); + + if (uploadResult.success) { + // 触发发送事件 + this.triggerEvent('send', { + type: 'voice', + url: uploadResult.url, + duration: uploadResult.duration, + size: uploadResult.size, + tempFilePath: this.data.tempFilePath + }); + + // 关闭录音界面 + this.closeRecorder(); + + wx.showToast({ + title: '发送成功', + icon: 'success' + }); + + } else { + throw new Error('上传失败'); + } + + } catch (error) { + wx.hideLoading(); + console.error('📤 发送录音失败:', error); + + wx.showToast({ + title: '发送失败', + icon: 'none' + }); + } + }, + + // 🎨 ===== 界面控制 ===== + + // 关闭录音界面 + closeRecorder() { + console.log('❌ 关闭录音界面'); + + // 如果正在录音,先停止 + if (this.data.recordingState === 'recording') { + this.cancelRecording(); + } + + // 如果正在播放,先停止 + if (this.data.isPlaying) { + voiceMessageManager.stopPlaying(); + } + + // 先自隐,再通知父级,提升关闭成功率 + this.setData({ visible: false }); + this.triggerEvent('close'); + }, + + // 遮罩点击 + onOverlayTap() { + // 点击遮罩关闭 + this.closeRecorder(); + }, + + // 🔐 ===== 权限处理 ===== + + // 取消权限申请 + cancelPermission() { + this.setData({ + showPermissionGuide: false + }); + }, + + // 打开设置页面 + openSettings() { + wx.openSetting({ + success: (res) => { + if (res.authSetting['scope.record']) { + this.setData({ + showPermissionGuide: false + }); + + wx.showToast({ + title: '权限已开启', + icon: 'success' + }); + } + } + }); + }, + + // 🔧 ===== 工具方法 ===== + + // 重置录音器 + resetRecorder() { + this.setData({ + recordingState: 'idle', + statusText: '准备录音', + recordingDuration: 0, + tempFilePath: '', + fileSize: 0, + isPlaying: false, + errorMessage: '', + showPermissionGuide: false + }); + + this.stopTimers(); + this.initWaveform(); + }, + + // 初始化波形 + initWaveform() { + const waveform = Array(20).fill(0).map(() => Math.random() * 30 + 10); + this.setData({ + realtimeWaveform: waveform, + finalWaveform: [] + }); + }, + + // 开始时长计时器 + startDurationTimer() { + this.stopTimers(); + + this.data.durationTimer = setInterval(() => { + const duration = this.data.recordingDuration + 100; + this.setData({ + recordingDuration: duration + }); + + // 检查最大时长 + if (duration >= this.properties.maxDuration) { + this.stopRecording(); + } + }, 100); + }, + + // 开始波形动画 + startWaveformAnimation() { + this.data.waveformTimer = setInterval(() => { + const waveform = Array(20).fill(0).map(() => Math.random() * 80 + 20); + this.setData({ + realtimeWaveform: waveform + }); + }, 150); + }, + + // 停止定时器 + stopTimers() { + if (this.data.durationTimer) { + clearInterval(this.data.durationTimer); + this.data.durationTimer = null; + } + + if (this.data.waveformTimer) { + clearInterval(this.data.waveformTimer); + this.data.waveformTimer = null; + } + }, + + // 更新实时波形 + updateRealtimeWaveform(frameBuffer) { + if (!frameBuffer) return; + + // 简化的波形数据处理 + const waveform = Array(20).fill(0).map(() => Math.random() * 80 + 20); + this.setData({ + realtimeWaveform: waveform + }); + }, + + // 生成最终波形 + generateFinalWaveform() { + const duration = this.data.recordingDuration; + const barCount = Math.min(Math.max(Math.floor(duration / 200), 15), 40); + + const waveform = Array(barCount).fill(0).map(() => Math.random() * 70 + 15); + this.setData({ + finalWaveform: waveform + }); + }, + + // 格式化时长 + formatDuration(duration) { + if (!duration || duration <= 0) return '00:00'; + + const totalSeconds = Math.floor(duration / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + + return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + }, + + // 获取文件大小文本 + getFileSizeText(fileSize) { + if (!fileSize || fileSize <= 0) return ''; + + if (fileSize < 1024) { + return `${fileSize}B`; + } else if (fileSize < 1024 * 1024) { + return `${(fileSize / 1024).toFixed(1)}KB`; + } else { + return `${(fileSize / (1024 * 1024)).toFixed(1)}MB`; + } + }, + + // 获取质量文本 + getQualityText(duration) { + if (!duration || duration <= 0) return ''; + + const seconds = Math.floor(duration / 1000); + if (seconds < 3) return '音质:一般'; + if (seconds < 10) return '音质:良好'; + return '音质:优秀'; + }, + + // 获取错误消息 + getErrorMessage(error) { + if (error.message) { + if (error.message.includes('权限')) { + return '需要录音权限'; + } else if (error.message.includes('timeout')) { + return '录音超时'; + } else if (error.message.includes('fail')) { + return '录音失败'; + } + return error.message; + } + return '未知错误'; + }, + + // 清理资源 + cleanup() { + this.stopTimers(); + + // 如果正在录音,取消录音 + if (this.data.recordingState === 'recording') { + voiceMessageManager.cancelRecording(); + } + + // 如果正在播放,停止播放 + if (this.data.isPlaying) { + voiceMessageManager.stopPlaying(); + } + + // 移除事件监听器 + voiceMessageManager.off('recordStart'); + voiceMessageManager.off('recordStop'); + voiceMessageManager.off('recordPause'); + voiceMessageManager.off('recordResume'); + voiceMessageManager.off('recordError'); + voiceMessageManager.off('recordFrame'); + } + } +}); diff --git a/components/voice-recorder/voice-recorder.json b/components/voice-recorder/voice-recorder.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/components/voice-recorder/voice-recorder.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/components/voice-recorder/voice-recorder.wxml b/components/voice-recorder/voice-recorder.wxml new file mode 100644 index 0000000..1c76ec3 --- /dev/null +++ b/components/voice-recorder/voice-recorder.wxml @@ -0,0 +1,150 @@ + + + + + + + + + + + 🎤 + + + + ⏸️ + + + + + {{statusText}} + + + + + {{formatDuration(recordingDuration)}} + / {{formatDuration(maxDuration)}} + + + + + + + + + + + + + + + + + + + + + {{getFileSizeText(fileSize)}} + {{getQualityText(recordingDuration)}} + + + + + + + + 按住录音 + + + + + + 暂停 + + + 取消 + + + 完成 + + + + + + + 继续 + + + 取消 + + + 完成 + + + + + + + {{isPlaying ? '暂停' : '试听'}} + + + 重录 + + + 发送 + + + + + + + + 按住录音按钮开始录制,最长{{Math.floor(maxDuration/1000)}}秒 + + + 松开结束录音,向上滑动取消 + + + 录音已暂停,可以继续录制或完成录音 + + + 录音完成,可以试听或发送 + + + {{errorMessage}} + + + + + + × + + + + + + + 🎤 + 需要录音权限 + 使用语音消息功能需要录音权限,请在设置中开启 + + + 取消 + + + 去设置 + + + + + diff --git a/components/voice-recorder/voice-recorder.wxss b/components/voice-recorder/voice-recorder.wxss new file mode 100644 index 0000000..585de3b --- /dev/null +++ b/components/voice-recorder/voice-recorder.wxss @@ -0,0 +1,534 @@ +/* 🎤 语音录制组件样式 */ + +/* CSS变量定义 */ +.voice-recorder-container { + --primary-color: #007AFF; + --primary-light: #5AC8FA; + --primary-dark: #0051D5; + --success-color: #34C759; + --warning-color: #FF9500; + --danger-color: #FF3B30; + --background-color: #F2F2F7; + --surface-color: #FFFFFF; + --text-primary: #000000; + --text-secondary: #8E8E93; + --text-tertiary: #C7C7CC; + --border-color: #E5E5EA; + --shadow-light: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); + --shadow-medium: 0 8rpx 24rpx rgba(0, 0, 0, 0.15); + --radius-small: 8rpx; + --radius-medium: 12rpx; + --radius-large: 20rpx; + --radius-xl: 32rpx; +} + +/* 🌙 深色模式支持 */ +@media (prefers-color-scheme: dark) { + .voice-recorder-container { + --primary-color: #0A84FF; + --background-color: #000000; + --surface-color: #1C1C1E; + --text-primary: #FFFFFF; + --text-secondary: #8E8E93; + --text-tertiary: #48484A; + --border-color: #38383A; + --shadow-light: 0 2rpx 8rpx rgba(0, 0, 0, 0.3); + --shadow-medium: 0 8rpx 24rpx rgba(0, 0, 0, 0.4); + } +} + +.voice-recorder-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.voice-recorder-container.visible { + opacity: 1; + pointer-events: auto; +} + +.voice-recorder-container.hidden { + opacity: 0; + pointer-events: none; +} + +/* 🎨 录音遮罩 */ +.recorder-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(10rpx); +} + +/* 🎨 录音内容 */ +.recorder-content { + width: 640rpx; + max-width: 90vw; + background: var(--surface-color); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-medium); + padding: 60rpx 40rpx 40rpx; + position: relative; + animation: slideUp 0.3s ease-out; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(100rpx) scale(0.9); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* 🎤 录音状态 */ +.recorder-status { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 40rpx; +} + +.status-icon { + width: 120rpx; + height: 120rpx; + border-radius: 60rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 24rpx; + transition: all 0.3s ease; + position: relative; +} + +.status-icon.idle { + background: var(--background-color); + border: 2rpx solid var(--border-color); +} + +.status-icon.recording { + background: linear-gradient(135deg, var(--danger-color) 0%, #FF6B6B 100%); + animation: pulse 2s infinite; +} + +.status-icon.paused { + background: var(--warning-color); +} + +.status-icon.completed { + background: var(--success-color); +} + +.status-icon.error { + background: var(--danger-color); +} + +@keyframes pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.1); } +} + +.icon-microphone { + font-size: 48rpx; +} + +.icon-recording { + position: relative; +} + +.recording-dot { + width: 24rpx; + height: 24rpx; + border-radius: 12rpx; + background: white; + animation: blink 1s infinite; +} + +@keyframes blink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0.3; } +} + +.icon-paused, +.icon-completed, +.icon-error { + font-size: 48rpx; + color: white; +} + +.status-text { + font-size: 32rpx; + font-weight: 600; + color: var(--text-primary); + text-align: center; +} + +/* ⏱️ 录音时长 */ +.recording-duration { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 40rpx; + gap: 8rpx; +} + +.duration-text { + font-size: 48rpx; + font-weight: 700; + color: var(--primary-color); + font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; +} + +.max-duration-text { + font-size: 28rpx; + color: var(--text-secondary); + font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; +} + +/* 🌊 实时波形 */ +.realtime-waveform { + height: 120rpx; + margin-bottom: 40rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.waveform-container { + display: flex; + align-items: center; + justify-content: center; + gap: 6rpx; + height: 100%; +} + +.wave-bar { + width: 8rpx; + border-radius: 4rpx; + transition: all 0.2s ease; + min-height: 16rpx; +} + +.wave-bar.realtime { + background: var(--primary-color); + animation: waveAnimation 1s ease-in-out infinite; +} + +.wave-bar.preview { + background: var(--text-tertiary); +} + +@keyframes waveAnimation { + 0%, 100% { transform: scaleY(0.5); } + 50% { transform: scaleY(1); } +} + +/* 📊 录音预览 */ +.recording-preview { + margin-bottom: 40rpx; +} + +.preview-waveform { + height: 80rpx; + margin-bottom: 24rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.preview-info { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 20rpx; +} + +.file-size, +.quality-text { + font-size: 24rpx; + color: var(--text-secondary); +} + +/* 🎛️ 录音控制 */ +.recorder-controls { + margin-bottom: 32rpx; +} + +.control-button { + height: 88rpx; + border-radius: var(--radius-medium); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 16rpx; + transition: all 0.3s ease; + user-select: none; + -webkit-user-select: none; +} + +.control-button:last-child { + margin-bottom: 0; +} + +.control-button.primary { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%); + color: white; + box-shadow: var(--shadow-light); +} + +.control-button.secondary { + background: var(--background-color); + color: var(--text-primary); + border: 1rpx solid var(--border-color); +} + +.control-button.danger { + background: rgba(255, 59, 48, 0.1); + color: var(--danger-color); + border: 1rpx solid rgba(255, 59, 48, 0.3); +} + +.control-button:active { + transform: scale(0.98); +} + +.control-button.primary:active { + background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-color) 100%); +} + +.button-text { + font-size: 32rpx; + font-weight: 600; +} + +/* 录音中的控制按钮 */ +.recording-controls, +.paused-controls, +.completed-controls { + display: flex; + gap: 16rpx; +} + +.recording-controls .control-button, +.paused-controls .control-button, +.completed-controls .control-button { + flex: 1; + margin-bottom: 0; +} + +/* 💡 录音提示 */ +.recorder-tips { + text-align: center; + margin-bottom: 20rpx; +} + +.tip-text { + font-size: 26rpx; + color: var(--text-secondary); + line-height: 1.4; +} + +.tip-text.error { + color: var(--danger-color); +} + +/* ❌ 关闭按钮 */ +.close-button { + position: absolute; + top: 20rpx; + right: 20rpx; + width: 60rpx; + height: 60rpx; + border-radius: 30rpx; + background: var(--background-color); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.close-button:active { + background: var(--border-color); + transform: scale(0.95); +} + +.close-icon { + font-size: 36rpx; + color: var(--text-secondary); + font-weight: 300; +} + +/* 🔐 权限引导 */ +.permission-guide { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-xl); +} + +.guide-content { + background: var(--surface-color); + border-radius: var(--radius-large); + padding: 60rpx 40rpx 40rpx; + margin: 40rpx; + text-align: center; + box-shadow: var(--shadow-medium); +} + +.guide-icon { + font-size: 80rpx; + margin-bottom: 24rpx; +} + +.guide-title { + font-size: 36rpx; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 16rpx; + display: block; +} + +.guide-desc { + font-size: 28rpx; + color: var(--text-secondary); + line-height: 1.4; + margin-bottom: 40rpx; + display: block; +} + +.guide-buttons { + display: flex; + gap: 16rpx; +} + +.guide-button { + flex: 1; + height: 80rpx; + border-radius: var(--radius-medium); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.guide-button.primary { + background: var(--primary-color); + color: white; +} + +.guide-button.secondary { + background: var(--background-color); + color: var(--text-primary); + border: 1rpx solid var(--border-color); +} + +.guide-button:active { + transform: scale(0.98); +} + +/* 📱 响应式设计 */ +@media screen and (max-width: 375px) { + .recorder-content { + width: 560rpx; + padding: 50rpx 30rpx 30rpx; + } + + .status-icon { + width: 100rpx; + height: 100rpx; + } + + .duration-text { + font-size: 40rpx; + } + + .realtime-waveform { + height: 100rpx; + } + + .control-button { + height: 76rpx; + } + + .button-text { + font-size: 28rpx; + } +} + +@media screen and (min-width: 414px) { + .recorder-content { + width: 720rpx; + padding: 70rpx 50rpx 50rpx; + } + + .status-icon { + width: 140rpx; + height: 140rpx; + } + + .duration-text { + font-size: 56rpx; + } + + .realtime-waveform { + height: 140rpx; + } + + .control-button { + height: 96rpx; + } + + .button-text { + font-size: 36rpx; + } +} + +/* 🎭 动画增强 */ +.voice-recorder-container.visible .recorder-content { + animation: slideUp 0.3s ease-out; +} + +.voice-recorder-container.hidden .recorder-content { + animation: slideDown 0.3s ease-in; +} + +@keyframes slideDown { + from { + opacity: 1; + transform: translateY(0) scale(1); + } + to { + opacity: 0; + transform: translateY(100rpx) scale(0.9); + } +} + +/* 触摸反馈 */ +.control-button { + -webkit-tap-highlight-color: transparent; +} + +/* 可访问性 */ +.control-button[aria-pressed="true"] { + outline: 2rpx solid var(--primary-color); + outline-offset: 4rpx; +} diff --git a/config/config.js b/config/config.js new file mode 100644 index 0000000..4be49f4 --- /dev/null +++ b/config/config.js @@ -0,0 +1,164 @@ +// 小程序配置文件 - 对应Fl`utter的app_config.dart +module.exports = { + // 应用信息 + appName: "FindMe", + appVersion: "1.0.0", + + // API配置 - 使用正确的域名 + api: { + baseUrl: "https://api.faxianwo.me", + timeout: 15000 + }, + + // WebSocket配置 - 使用正确的WebSocket路径 + websocket: { + baseUrl: "wss://api.faxianwo.me", + url: "wss://api.faxianwo.me/api/v1/ws", // 🔥 恢复正确的WebSocket路径 + heartbeatInterval: 30000, // 30秒 + reconnectBaseDelay: 2000, // 重连基础延迟 + maxReconnectAttempts: 10 // 最大重连次数 + }, + + // 高德地图配置 + //amapKey: '55e1d9a4c6c0f9fa4c75656fe3060641', + amapKey: '9212b693317725fca66ae697ec444fd5', + + // 网络超时设置 (保持向后兼容) + networkTimeout: { + request: 15000, + upload: 60000, + download: 60000 + }, + + // 位置刷新间隔 + locationUpdateInterval: 30000, // 30秒 + + // 文件大小限制 (小程序有自己的限制,这里作为前端验证) + fileSizeLimit: { + image: 10 * 1024 * 1024, // 10MB + video: 50 * 1024 * 1024, // 50MB + audio: 5 * 1024 * 1024, // 5MB + file: 50 * 1024 * 1024 // 50MB + }, + + // 支持的文件类型 + supportedFileTypes: { + image: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'], + video: ['mp4', 'avi', 'wmv', 'mov', '3gp'], + audio: ['mp3', 'wav', 'ogg', 'aac', 'm4a'] + }, + + // 缓存设置 + cache: { + maxAge: 7 * 24 * 60 * 60, // 7天 + resourceExpireTime: { + image: 4 * 3600, // 4小时 + video: 2 * 3600, // 2小时 + audio: 2 * 3600, // 2小时 + file: 1 * 3600, // 1小时 + avatar: 24 * 3600 // 24小时 + } + }, + + // API端点路径 + apiPaths: { + // 身份验证相关(统一使用user路径) + auth: { + sendVerifyCode: '/api/v1/user/send-verify-code', + login: '/api/v1/user/login', + wechatLogin: '/api/v1/user/wechat-login', + refresh: '/api/v1/auth/refresh', + logout: '/api/v1/user/logout' + }, + // 用户相关 + user: { + profile: '/api/v1/user/profile', + updateProfile: '/api/v1/user/profile', + setting: '/api/v1/user/setting', + bindPhone: '/api/v1/user/bind-phone', + detectMerge: '/api/v1/user/detect-merge', + mergeAccount: '/api/v1/user/merge-account' + }, + + // 位置相关 + location: { + update: '/api/v1/location/update', + getFriends: '/api/v1/location/friends', + getStrangers: '/api/v1/location/strangers', + privacy: '/api/v1/location/privacy' + }, + + // 聊天相关 - 根据接口文档修正API路径 + chat: { + conversations: '/api/v1/chat/conversations', + history: '/api/v1/chat/history', // 修正:使用正确的历史消息接口 + messages: '/api/v1/chat/history', // 保持向后兼容 + send: '/api/v1/chat/send', + batchRead: '/api/v1/chat/batch-read', // 新增:批量已读 + markAllRead: '/api/v1/chat/mark-all-read', // 新增:全部已读 + unreadTotal: '/api/v1/chat/unread/total', // 新增:总未读数 + settings: '/api/v1/chat/settings', // 新增:聊天设置 + backup: '/api/v1/chat/backup', // 新增:备份 + restore: '/api/v1/chat/restore', // 新增:恢复 + danmaku: '/api/v1/chat/danmaku', // 新增:弹幕 + emoji: '/api/v1/chat/emoji/packages', // 新增:表情包 + syncPull: '/api/v1/chat/sync/pull', // 新增:同步拉取 + syncAck: '/api/v1/chat/sync/ack', // 新增:同步确认 + upload: '/api/v1/file/upload' + }, + + // 社交相关 + social: { + friends: '/api/v1/social/friends', + addFriend: '/api/v1/social/friend/add', + acceptFriend: '/api/v1/social/friend/handle-request', + deleteFriend: '/api/v1/social/friend', + search: '/api/v1/social/search', + friendRequests: '/api/v1/social/friend/requests', + friendRequestsCount: '/api/v1/social/friend/requests/count', + groups: '/api/v1/social/groups', + groupsCount: '/api/v1/social/groups/count' + }, + + + }, + + // 小程序特有配置 + miniprogram: { + // 页面路径 + pages: { + splash: '/pages/splash/splash', + login: '/pages/login/login', + main: '/pages/main/main', + map: '/pages/map/map', + friends: '/pages/social/friends/friends', + message: '/pages/message/message', + profile: '/pages/profile/profile' + }, + + // 主题配置 + theme: { + primaryColor: '#3cc51f', + backgroundColor: '#F5F5F5', + textColor: '#333333', + subTextColor: '#999999' + }, + + // 地图配置 + map: { + defaultZoom: 16, + minZoom: 3, + maxZoom: 20, + showLocation: true, + showScale: true, + showCompass: true + } + }, + + // 调试配置 + debug: { + enabled: true, + showLog: true, + mockData: false + } +}; \ No newline at end of file diff --git a/images/1111.jpg b/images/1111.jpg new file mode 100644 index 0000000..60d5cdb Binary files /dev/null and b/images/1111.jpg differ diff --git a/images/Edit3.png b/images/Edit3.png new file mode 100644 index 0000000..ce9bb3f Binary files /dev/null and b/images/Edit3.png differ diff --git a/images/Group.png b/images/Group.png new file mode 100644 index 0000000..f52673c Binary files /dev/null and b/images/Group.png differ diff --git a/images/Icon.png b/images/Icon.png new file mode 100644 index 0000000..2bc4096 Binary files /dev/null and b/images/Icon.png differ diff --git a/images/QR_code.png b/images/QR_code.png new file mode 100644 index 0000000..71c13a4 Binary files /dev/null and b/images/QR_code.png differ diff --git a/images/Subtract.png b/images/Subtract.png new file mode 100644 index 0000000..7fd5f18 Binary files /dev/null and b/images/Subtract.png differ diff --git a/images/about.png b/images/about.png new file mode 100644 index 0000000..f52ad7d Binary files /dev/null and b/images/about.png differ diff --git a/images/album-bold.png b/images/album-bold.png new file mode 100644 index 0000000..547aaf0 Binary files /dev/null and b/images/album-bold.png differ diff --git a/images/btn.png b/images/btn.png new file mode 100644 index 0000000..bd6364f Binary files /dev/null and b/images/btn.png differ diff --git a/images/camera.png b/images/camera.png new file mode 100644 index 0000000..70e53f5 Binary files /dev/null and b/images/camera.png differ diff --git a/images/cameras.png b/images/cameras.png new file mode 100644 index 0000000..a0320e7 Binary files /dev/null and b/images/cameras.png differ diff --git a/images/chat.png b/images/chat.png new file mode 100644 index 0000000..9a8b5fe Binary files /dev/null and b/images/chat.png differ diff --git a/images/download.svg b/images/download.svg new file mode 100644 index 0000000..8c8ec13 --- /dev/null +++ b/images/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/emoji-lol.png b/images/emoji-lol.png new file mode 100644 index 0000000..0bb3e96 Binary files /dev/null and b/images/emoji-lol.png differ diff --git a/images/eye.png b/images/eye.png new file mode 100644 index 0000000..d9448ed Binary files /dev/null and b/images/eye.png differ diff --git a/images/findme-logo.png b/images/findme-logo.png new file mode 100644 index 0000000..c9248eb Binary files /dev/null and b/images/findme-logo.png differ diff --git a/images/folder-favorite.png b/images/folder-favorite.png new file mode 100644 index 0000000..3b8ff74 Binary files /dev/null and b/images/folder-favorite.png differ diff --git a/images/game-fill.png b/images/game-fill.png new file mode 100644 index 0000000..4ec4062 Binary files /dev/null and b/images/game-fill.png differ diff --git a/images/lang.png b/images/lang.png new file mode 100644 index 0000000..5813b77 Binary files /dev/null and b/images/lang.png differ diff --git a/images/location.png b/images/location.png new file mode 100644 index 0000000..32349e6 Binary files /dev/null and b/images/location.png differ diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000..c9248eb Binary files /dev/null and b/images/logo.png differ diff --git a/images/map/marker_blank.png b/images/map/marker_blank.png new file mode 100644 index 0000000..da6fe0b Binary files /dev/null and b/images/map/marker_blank.png differ diff --git a/images/map/marker_canguan.png b/images/map/marker_canguan.png new file mode 100644 index 0000000..2c31bd0 Binary files /dev/null and b/images/map/marker_canguan.png differ diff --git a/images/map/marker_hong.png b/images/map/marker_hong.png new file mode 100644 index 0000000..f53aa34 Binary files /dev/null and b/images/map/marker_hong.png differ diff --git a/images/map/marker_kafei.png b/images/map/marker_kafei.png new file mode 100644 index 0000000..cb91b6d Binary files /dev/null and b/images/map/marker_kafei.png differ diff --git a/images/map/marker_lan.png b/images/map/marker_lan.png new file mode 100644 index 0000000..b9f0cbc Binary files /dev/null and b/images/map/marker_lan.png differ diff --git a/images/map/marker_yinliao.png b/images/map/marker_yinliao.png new file mode 100644 index 0000000..2ad734e Binary files /dev/null and b/images/map/marker_yinliao.png differ diff --git a/images/map/marker_yule.png b/images/map/marker_yule.png new file mode 100644 index 0000000..fe902fd Binary files /dev/null and b/images/map/marker_yule.png differ diff --git a/images/moments.png b/images/moments.png new file mode 100644 index 0000000..1d2c184 Binary files /dev/null and b/images/moments.png differ diff --git a/images/news.png b/images/news.png new file mode 100644 index 0000000..c3a162c Binary files /dev/null and b/images/news.png differ diff --git a/images/placeholder.txt b/images/placeholder.txt new file mode 100644 index 0000000..12ade83 --- /dev/null +++ b/images/placeholder.txt @@ -0,0 +1,8 @@ +这个目录用于存放小程序的图片资源 + +需要从Flutter项目中复制以下文件: +1. assets/images/logo_light.png -> images/logo/logo_light.png +2. assets/images/default_avatar.png -> images/avatars/default_avatar.png +3. assets/images/user_marker.png -> images/markers/friend.png 和 stranger.png + +其他需要的图标文件可以使用简单的占位图片或从图标库下载。 \ No newline at end of file diff --git a/images/qr-code.png b/images/qr-code.png new file mode 100644 index 0000000..5d68a36 Binary files /dev/null and b/images/qr-code.png differ diff --git a/images/return.png b/images/return.png new file mode 100644 index 0000000..1f95144 Binary files /dev/null and b/images/return.png differ diff --git a/images/scan.svg b/images/scan.svg new file mode 100644 index 0000000..7a7323d --- /dev/null +++ b/images/scan.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/share.svg b/images/share.svg new file mode 100644 index 0000000..85ac4e6 --- /dev/null +++ b/images/share.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/shopping-bag.png b/images/shopping-bag.png new file mode 100644 index 0000000..d6ab75d Binary files /dev/null and b/images/shopping-bag.png differ diff --git a/images/subject.png b/images/subject.png new file mode 100644 index 0000000..bf39255 Binary files /dev/null and b/images/subject.png differ diff --git a/images/tag.png b/images/tag.png new file mode 100644 index 0000000..95f34ca Binary files /dev/null and b/images/tag.png differ diff --git a/images/view.png b/images/view.png new file mode 100644 index 0000000..449f08b Binary files /dev/null and b/images/view.png differ diff --git a/images/vip-card1.png b/images/vip-card1.png new file mode 100644 index 0000000..4d483f8 Binary files /dev/null and b/images/vip-card1.png differ diff --git a/images/vip-card2.png b/images/vip-card2.png new file mode 100644 index 0000000..0ee8206 Binary files /dev/null and b/images/vip-card2.png differ diff --git a/images/vip-card3.png b/images/vip-card3.png new file mode 100644 index 0000000..0effbd4 Binary files /dev/null and b/images/vip-card3.png differ diff --git a/images/wallet-solid.png b/images/wallet-solid.png new file mode 100644 index 0000000..4f90e4e Binary files /dev/null and b/images/wallet-solid.png differ diff --git a/libs/amap-wx-all.js b/libs/amap-wx-all.js new file mode 100644 index 0000000..718d6ef --- /dev/null +++ b/libs/amap-wx-all.js @@ -0,0 +1,31 @@ +function AMapWX(a){this.key=a.key;this.requestConfig={key:a.key,s:"rsx",platform:"WXJS",appname:a.key,sdkversion:"1.2.0",logversion:"2.0"};this.MeRequestConfig={key:a.key,serviceName:"https://restapi.amap.com/rest/me"}} +AMapWX.prototype.getWxLocation=function(a,b){wx.getLocation({type:"gcj02",success:function(c){c=c.longitude+","+c.latitude;wx.setStorage({key:"userLocation",data:c});b(c)},fail:function(c){wx.getStorage({key:"userLocation",success:function(d){d.data&&b(d.data)}});a.fail({errCode:"0",errMsg:c.errMsg||""})}})}; +AMapWX.prototype.getMEKeywordsSearch=function(a){if(!a.options)return a.fail({errCode:"0",errMsg:"\u7f3a\u5c11\u5fc5\u8981\u53c2\u6570"});var b=a.options,c=this.MeRequestConfig,d={key:c.key,s:"rsx",platform:"WXJS",appname:a.key,sdkversion:"1.2.0",logversion:"2.0"};b.layerId&&(d.layerId=b.layerId);b.keywords&&(d.keywords=b.keywords);b.city&&(d.city=b.city);b.filter&&(d.filter=b.filter);b.sortrule&&(d.sortrule=b.sortrule);b.pageNum&&(d.pageNum=b.pageNum);b.pageSize&&(d.pageSize=b.pageSize);b.sig&&(d.sig= +b.sig);wx.request({url:c.serviceName+"/cpoint/datasearch/local",data:d,method:"GET",header:{"content-type":"application/json"},success:function(e){(e=e.data)&&e.status&&"1"===e.status&&0===e.code?a.success(e.data):a.fail({errCode:"0",errMsg:e})},fail:function(e){a.fail({errCode:"0",errMsg:e.errMsg||""})}})}; +AMapWX.prototype.getMEIdSearch=function(a){if(!a.options)return a.fail({errCode:"0",errMsg:"\u7f3a\u5c11\u5fc5\u8981\u53c2\u6570"});var b=a.options,c=this.MeRequestConfig,d={key:c.key,s:"rsx",platform:"WXJS",appname:a.key,sdkversion:"1.2.0",logversion:"2.0"};b.layerId&&(d.layerId=b.layerId);b.id&&(d.id=b.id);b.sig&&(d.sig=b.sig);wx.request({url:c.serviceName+"/cpoint/datasearch/id",data:d,method:"GET",header:{"content-type":"application/json"},success:function(e){(e=e.data)&&e.status&&"1"===e.status&& +0===e.code?a.success(e.data):a.fail({errCode:"0",errMsg:e})},fail:function(e){a.fail({errCode:"0",errMsg:e.errMsg||""})}})}; +AMapWX.prototype.getMEPolygonSearch=function(a){if(!a.options)return a.fail({errCode:"0",errMsg:"\u7f3a\u5c11\u5fc5\u8981\u53c2\u6570"});var b=a.options,c=this.MeRequestConfig,d={key:c.key,s:"rsx",platform:"WXJS",appname:a.key,sdkversion:"1.2.0",logversion:"2.0"};b.layerId&&(d.layerId=b.layerId);b.keywords&&(d.keywords=b.keywords);b.polygon&&(d.polygon=b.polygon);b.filter&&(d.filter=b.filter);b.sortrule&&(d.sortrule=b.sortrule);b.pageNum&&(d.pageNum=b.pageNum);b.pageSize&&(d.pageSize=b.pageSize); +b.sig&&(d.sig=b.sig);wx.request({url:c.serviceName+"/cpoint/datasearch/polygon",data:d,method:"GET",header:{"content-type":"application/json"},success:function(e){(e=e.data)&&e.status&&"1"===e.status&&0===e.code?a.success(e.data):a.fail({errCode:"0",errMsg:e})},fail:function(e){a.fail({errCode:"0",errMsg:e.errMsg||""})}})}; +AMapWX.prototype.getMEaroundSearch=function(a){if(!a.options)return a.fail({errCode:"0",errMsg:"\u7f3a\u5c11\u5fc5\u8981\u53c2\u6570"});var b=a.options,c=this.MeRequestConfig,d={key:c.key,s:"rsx",platform:"WXJS",appname:a.key,sdkversion:"1.2.0",logversion:"2.0"};b.layerId&&(d.layerId=b.layerId);b.keywords&&(d.keywords=b.keywords);b.center&&(d.center=b.center);b.radius&&(d.radius=b.radius);b.filter&&(d.filter=b.filter);b.sortrule&&(d.sortrule=b.sortrule);b.pageNum&&(d.pageNum=b.pageNum);b.pageSize&& +(d.pageSize=b.pageSize);b.sig&&(d.sig=b.sig);wx.request({url:c.serviceName+"/cpoint/datasearch/around",data:d,method:"GET",header:{"content-type":"application/json"},success:function(e){(e=e.data)&&e.status&&"1"===e.status&&0===e.code?a.success(e.data):a.fail({errCode:"0",errMsg:e})},fail:function(e){a.fail({errCode:"0",errMsg:e.errMsg||""})}})}; +AMapWX.prototype.getGeo=function(a){var b=this.requestConfig,c=a.options;b={key:this.key,extensions:"all",s:b.s,platform:b.platform,appname:this.key,sdkversion:b.sdkversion,logversion:b.logversion};c.address&&(b.address=c.address);c.city&&(b.city=c.city);c.batch&&(b.batch=c.batch);c.sig&&(b.sig=c.sig);wx.request({url:"https://restapi.amap.com/v3/geocode/geo",data:b,method:"GET",header:{"content-type":"application/json"},success:function(d){(d=d.data)&&d.status&&"1"===d.status?a.success(d):a.fail({errCode:"0", +errMsg:d})},fail:function(d){a.fail({errCode:"0",errMsg:d.errMsg||""})}})}; +AMapWX.prototype.getRegeo=function(a){function b(d){var e=c.requestConfig;wx.request({url:"https://restapi.amap.com/v3/geocode/regeo",data:{key:c.key,location:d,extensions:"all",s:e.s,platform:e.platform,appname:c.key,sdkversion:e.sdkversion,logversion:e.logversion},method:"GET",header:{"content-type":"application/json"},success:function(g){if(g.data.status&&"1"==g.data.status){g=g.data.regeocode;var h=g.addressComponent,f=[],k=g.roads[0].name+"\u9644\u8fd1",m=d.split(",")[0],n=d.split(",")[1];if(g.pois&& +g.pois[0]){k=g.pois[0].name+"\u9644\u8fd1";var l=g.pois[0].location;l&&(m=parseFloat(l.split(",")[0]),n=parseFloat(l.split(",")[1]))}h.provice&&f.push(h.provice);h.city&&f.push(h.city);h.district&&f.push(h.district);h.streetNumber&&h.streetNumber.street&&h.streetNumber.number?(f.push(h.streetNumber.street),f.push(h.streetNumber.number)):f.push(g.roads[0].name);f=f.join("");a.success([{iconPath:a.iconPath,width:a.iconWidth,height:a.iconHeight,name:f,desc:k,longitude:m,latitude:n,id:0,regeocodeData:g}])}else a.fail({errCode:g.data.infocode, +errMsg:g.data.info})},fail:function(g){a.fail({errCode:"0",errMsg:g.errMsg||""})}})}var c=this;a.location?b(a.location):c.getWxLocation(a,function(d){b(d)})}; +AMapWX.prototype.getWeather=function(a){function b(g){var h="base";a.type&&"forecast"==a.type&&(h="all");wx.request({url:"https://restapi.amap.com/v3/weather/weatherInfo",data:{key:d.key,city:g,extensions:h,s:e.s,platform:e.platform,appname:d.key,sdkversion:e.sdkversion,logversion:e.logversion},method:"GET",header:{"content-type":"application/json"},success:function(f){if(f.data.status&&"1"==f.data.status)if(f.data.lives){if((f=f.data.lives)&&0 { + console.log('[AMapWX] 微信定位成功:', res); + + const location = { + longitude: res.longitude, + latitude: res.latitude, + accuracy: res.accuracy || 0, + altitude: res.altitude || 0, + speed: res.speed || 0, + horizontalAccuracy: res.horizontalAccuracy || 0, + verticalAccuracy: res.verticalAccuracy || 0 + }; + + if (typeof callback === 'function') { + callback(location, null); + } + }, + fail: (err) => { + console.error('[AMapWX] 微信定位失败:', err); + + if (typeof callback === 'function') { + callback(null, err); + } + } + }); + } + + /** + * 计算两点间距离 + * @param {object} point1 - 起点 {longitude, latitude} + * @param {object} point2 - 终点 {longitude, latitude} + * @returns {number} 距离(米) + */ + calculateDistance(point1, point2) { + const R = 6371000; // 地球半径(米) + const lat1 = point1.latitude * Math.PI / 180; + const lat2 = point2.latitude * Math.PI / 180; + const deltaLat = (point2.latitude - point1.latitude) * Math.PI / 180; + const deltaLng = (point2.longitude - point1.longitude) * Math.PI / 180; + + const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + + Math.cos(lat1) * Math.cos(lat2) * + Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return R * c; + } +} + +// 创建单例实例 +let amapInstance = null; + +const AmapWX = { + /** + * 获取SDK实例 + * @param {object} options - 配置选项 + * @returns {AMapWX} + */ + getInstance(options = {}) { + if (!amapInstance) { + amapInstance = new AMapWX(options); + } + return amapInstance; + }, + + /** + * 重置SDK实例 + * @param {object} options - 配置选项 + * @returns {AMapWX} + */ + resetInstance(options = {}) { + amapInstance = new AMapWX(options); + return amapInstance; + } +}; + +// 导出模块 +module.exports = { + AMapWX, + AmapWX +}; \ No newline at end of file diff --git a/minitest/test.config.json b/minitest/test.config.json new file mode 100644 index 0000000..41cf3d0 --- /dev/null +++ b/minitest/test.config.json @@ -0,0 +1,3 @@ +{ + "treeData": [] +} \ No newline at end of file diff --git a/pages/account-sync/phone-binding/phone-binding.js b/pages/account-sync/phone-binding/phone-binding.js new file mode 100644 index 0000000..87bb444 --- /dev/null +++ b/pages/account-sync/phone-binding/phone-binding.js @@ -0,0 +1,490 @@ +// 手机号绑定页面 +const app = getApp(); +const apiClient = require('../../../utils/api-client.js'); +const accountSyncManager = require('../../../utils/account-sync.js'); +const authManager = require('../../../utils/auth.js'); + +Page({ + data: { + // 表单数据 + phoneNumber: '', + verifyCode: '', + + // 状态控制 + canSendCode: false, + canBind: false, + isBinding: false, + + // 验证码倒计时 + codeButtonText: '获取验证码', + codeCountdown: 0, + codeTimer: null, + + // 合并相关 + showMergeDialog: false, + mergeCandidates: [], + + // 系统适配 + statusBarHeight: 44, + navBarHeight: 88, + windowHeight: 667, + safeAreaBottom: 0, + + // 用户信息 + userInfo: null + }, + + onLoad: function (options) { + console.log('手机号绑定页面加载'); + this.initSystemInfo(); + this.initUserInfo(); + }, + + onUnload: function () { + // 清理定时器 + if (this.data.codeTimer) { + clearInterval(this.data.codeTimer); + } + }, + + // 初始化系统信息 + initSystemInfo() { + try { + const systemInfo = wx.getSystemInfoSync(); + const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); + + const statusBarHeight = systemInfo.statusBarHeight; + const navBarHeight = menuButtonInfo.bottom + menuButtonInfo.top - statusBarHeight; + + this.setData({ + statusBarHeight, + navBarHeight, + windowHeight: systemInfo.windowHeight, + safeAreaBottom: systemInfo.safeArea ? systemInfo.screenHeight - systemInfo.safeArea.bottom : 0 + }); + } catch (error) { + console.error('初始化系统信息失败:', error); + } + }, + + // 初始化用户信息 + initUserInfo() { + const userInfo = authManager.getUserDisplayInfo(); + if (userInfo) { + this.setData({ userInfo }); + } else { + console.error('无法获取用户信息,返回上一页'); + wx.navigateBack(); + } + }, + + // 手机号输入处理 + onPhoneInput: function (e) { + const value = e.detail.value; + this.setData({ + phoneNumber: value + }); + this.validateForm(); + }, + + // 验证码输入处理 + onCodeInput: function (e) { + const value = e.detail.value; + this.setData({ + verifyCode: value + }); + this.validateForm(); + }, + + // 表单验证 + validateForm: function () { + const { phoneNumber, verifyCode, codeCountdown } = this.data; + + // 手机号格式验证 + const phoneRegex = /^1[3-9]\d{9}$/; + const isPhoneValid = phoneRegex.test(phoneNumber); + + // 验证码验证 + const isCodeValid = verifyCode.length === 6; + + this.setData({ + canSendCode: isPhoneValid && codeCountdown === 0, + canBind: isPhoneValid && isCodeValid + }); + }, + + // 发送验证码 + sendVerifyCode: function () { + if (!this.data.canSendCode) { + return; + } + + const { phoneNumber } = this.data; + + wx.showLoading({ + title: '发送中...', + mask: true + }); + + apiClient.sendVerifyCode(phoneNumber) + .then(response => { + wx.hideLoading(); + console.log('验证码发送成功:', response); + + this.startCodeCountdown(); + + wx.showToast({ + title: '验证码已发送', + icon: 'success', + duration: 2000 + }); + }) + .catch(error => { + wx.hideLoading(); + console.error('验证码发送失败:', error); + + let errorMessage = '发送失败,请重试'; + if (error.message) { + errorMessage = error.message; + } + + wx.showToast({ + title: errorMessage, + icon: 'none', + duration: 3000 + }); + }); + }, + + // 开始验证码倒计时 + startCodeCountdown: function () { + let countdown = 60; + + this.setData({ + codeCountdown: countdown, + codeButtonText: `${countdown}s后重发` + }); + + const timer = setInterval(() => { + countdown--; + + if (countdown <= 0) { + clearInterval(timer); + this.setData({ + codeCountdown: 0, + codeButtonText: '重新发送', + codeTimer: null + }); + this.validateForm(); + } else { + this.setData({ + codeCountdown: countdown, + codeButtonText: `${countdown}s后重发` + }); + } + }, 1000); + + this.setData({ + codeTimer: timer + }); + }, + + // 绑定手机号 + handleBind: function () { + if (!this.data.canBind || this.data.isBinding) { + return; + } + + const { phoneNumber, verifyCode } = this.data; + + this.setData({ isBinding: true }); + + wx.showLoading({ + title: '绑定中...', + mask: true + }); + + // 🔥 使用新的分步绑定逻辑 + this.attemptBinding(phoneNumber, verifyCode); + }, + + // 🔥 分步绑定逻辑 + async attemptBinding(phoneNumber, verifyCode) { + try { + // 第一步:尝试不自动合并的绑定 + console.log('🔄 第一步:尝试绑定(不自动合并)'); + const result = await accountSyncManager.bindPhone(phoneNumber, verifyCode, false); + + // 绑定成功 + this.handleBindingSuccess(result); + + } catch (error) { + console.log('🔄 第一步绑定失败:', error.message); + + if (error.message && error.message.includes('已关联其他账号')) { + // 发现冲突,询问用户是否合并 + this.showMergeConfirmDialog(phoneNumber, verifyCode); + } else { + // 其他错误,直接显示 + wx.hideLoading(); + wx.showToast({ + title: error.message || '绑定失败', + icon: 'none', + duration: 3000 + }); + this.setData({ isBinding: false }); + } + } + }, + + // 显示合并确认对话框 + showMergeConfirmDialog(phoneNumber, verifyCode) { + wx.hideLoading(); + + wx.showModal({ + title: '发现账号冲突', + content: `手机号 ${phoneNumber} 已被其他账号使用。\n\n是否同意自动合并账号?合并后将保留当前账号的数据。`, + cancelText: '取消绑定', + confirmText: '同意合并', + success: (res) => { + if (res.confirm) { + // 用户同意合并 + this.performAutoMergeBinding(phoneNumber, verifyCode); + } else { + // 用户取消 + this.setData({ isBinding: false }); + } + } + }); + }, + + // 执行自动合并绑定 + async performAutoMergeBinding(phoneNumber, verifyCode) { + wx.showLoading({ + title: '合并账号中...', + mask: true + }); + + try { + console.log('🔄 第二步:执行自动合并绑定'); + const result = await accountSyncManager.bindPhone(phoneNumber, verifyCode, true); + + // 合并成功 + this.handleBindingSuccess(result); + + } catch (error) { + console.error('🔄 自动合并绑定失败:', error); + wx.hideLoading(); + wx.showToast({ + title: error.message || '合并失败', + icon: 'none', + duration: 3000 + }); + this.setData({ isBinding: false }); + } + }, + + // 处理绑定成功 + handleBindingSuccess(result) { + wx.hideLoading(); + console.log('✅ 绑定成功:', result); + + // 重置绑定状态 + this.setData({ isBinding: false }); + + // 根据文档,处理不同的绑定结果 + if (result.hasMerged && result.mergeCount > 0) { + wx.showToast({ + title: `绑定成功!已自动合并 ${result.mergeCount} 个账号`, + icon: 'success', + duration: 2000 + }); + } else { + wx.showToast({ + title: '绑定成功!', + icon: 'success', + duration: 2000 + }); + } + + // 延迟返回上一页,让用户看到成功提示 + setTimeout(() => { + wx.navigateBack(); + }, 2000); + }, + + // 处理账号冲突 + async handleAccountConflict() { + try { + const userInfo = this.data.userInfo; + if (!userInfo || !userInfo.customId) { + throw new Error('无法获取用户ID'); + } + + console.log('🔍 检测可合并账号 - 详细信息:', { + currentUserCustomId: userInfo.customId, + phoneNumber: this.data.phoneNumber, + currentUserPhone: userInfo.phone + }); + + const mergeInfo = await accountSyncManager.detectMerge(userInfo.customId); + + console.log('🔍 检测结果详情:', { + hasMergeCandidates: mergeInfo.hasMergeCandidates, + candidatesCount: mergeInfo.candidates?.length || 0, + candidates: mergeInfo.candidates, + autoMerged: mergeInfo.autoMerged, + mergeCount: mergeInfo.mergeCount + }); + + if (mergeInfo.hasMergeCandidates && mergeInfo.candidates.length > 0) { + this.setData({ + mergeCandidates: mergeInfo.candidates, + showMergeDialog: true + }); + } else { + // 🚨 异常情况:绑定失败但检测不到冲突账号 + console.error('🚨 系统异常:绑定失败但检测不到冲突账号'); + + wx.showModal({ + title: '系统检测异常', + content: `手机号 ${this.data.phoneNumber} 绑定失败,提示已被其他账号使用,但系统检测不到冲突账号。\n\n可能原因:\n1. 数据库状态异常\n2. 已删除账号的残留数据\n3. 账号状态不一致\n\n建议联系技术支持处理。`, + showCancel: true, + cancelText: '使用其他手机号', + confirmText: '联系客服', + success: (res) => { + if (res.confirm) { + wx.showToast({ + title: '请联系客服处理此问题', + icon: 'none', + duration: 3000 + }); + } + } + }); + } + } catch (error) { + console.error('检测可合并账号失败:', error); + wx.showToast({ + title: '检测账号失败,请重试', + icon: 'none', + duration: 3000 + }); + } + }, + + // 关闭合并对话框 + closeMergeDialog: function () { + this.setData({ + showMergeDialog: false, + mergeCandidates: [] + }); + }, + + // 手动合并指定账号 + mergeSpecificAccount: function (e) { + const candidateCustomId = e.currentTarget.dataset.customId; + this.performMerge(candidateCustomId, '用户手动选择合并'); + }, + + // 自动合并所有账号 + autoMergeAll: function () { + const { phoneNumber, verifyCode } = this.data; + + wx.showLoading({ + title: '自动合并中...', + mask: true + }); + + // 根据文档,使用autoMerge=true参数进行自动合并 + accountSyncManager.bindPhone(phoneNumber, verifyCode, true) + .then(result => { + wx.hideLoading(); + console.log('自动合并成功:', result); + + const mergeCount = result.mergeCount || 0; + const message = result.hasMerged ? + `绑定成功!已自动合并 ${mergeCount} 个账号` : + '绑定成功!'; + + wx.showToast({ + title: message, + icon: 'success', + duration: 2000 + }); + + this.setData({ showMergeDialog: false }); + + setTimeout(() => { + wx.navigateBack(); + }, 2000); + }) + .catch(error => { + wx.hideLoading(); + console.error('自动合并失败:', error); + + wx.showToast({ + title: error.message || '自动合并失败', + icon: 'none', + duration: 3000 + }); + }); + }, + + // 执行合并操作 + async performMerge(secondaryCustomId, mergeReason) { + try { + const userInfo = this.data.userInfo; + if (!userInfo || !userInfo.customId) { + throw new Error('无法获取用户ID'); + } + + wx.showLoading({ + title: '合并中...', + mask: true + }); + + console.log('执行账号合并:', { + primary: userInfo.customId, + secondary: secondaryCustomId, + reason: mergeReason + }); + + const result = await accountSyncManager.mergeAccount( + userInfo.customId, + secondaryCustomId, + mergeReason + ); + + wx.hideLoading(); + console.log('账号合并成功:', result); + + wx.showToast({ + title: '账号合并成功!', + icon: 'success', + duration: 2000 + }); + + this.setData({ showMergeDialog: false }); + + setTimeout(() => { + wx.navigateBack(); + }, 2000); + + } catch (error) { + wx.hideLoading(); + console.error('账号合并失败:', error); + + wx.showToast({ + title: error.message || '合并失败,请重试', + icon: 'none', + duration: 3000 + }); + } + }, + + // 返回上一页 + goBack: function () { + wx.navigateBack(); + } +}); diff --git a/pages/account-sync/phone-binding/phone-binding.json b/pages/account-sync/phone-binding/phone-binding.json new file mode 100644 index 0000000..d2477f8 --- /dev/null +++ b/pages/account-sync/phone-binding/phone-binding.json @@ -0,0 +1,5 @@ +{ + "navigationStyle": "custom", + "backgroundColor": "#667eea", + "backgroundTextStyle": "light" +} diff --git a/pages/account-sync/phone-binding/phone-binding.wxml b/pages/account-sync/phone-binding/phone-binding.wxml new file mode 100644 index 0000000..0213e25 --- /dev/null +++ b/pages/account-sync/phone-binding/phone-binding.wxml @@ -0,0 +1,141 @@ + + + + + + + + + + 绑定手机号 + + + + + + + + + + 📱 + 绑定手机号 + 为了更好的使用体验和账号安全,请绑定您的手机号 + + + + + + + + 手机号 + + 📱 + + + + + + + 验证码 + + 🔐 + + + {{codeButtonText}} + + + + + + + + + 🔗 + {{isBinding ? '绑定中...' : '立即绑定'}} + + + + + + + + 温馨提示 + • 绑定手机号后可以更安全地管理您的账号 + • 如果该手机号已关联其他账号,系统会提示您进行账号合并 + • 账号合并后,所有数据将统一到当前账号 + + + + + + + + + + 发现可合并的账号 + × + + + + 该手机号已关联以下账号,请选择处理方式: + + + + + + {{item.nickname || '未知用户'}} + + 注册时间:{{item.registerTime}} + 匹配原因:{{item.matchReason}} + 可信度:{{item.confidence}}% + + + + 合并此账号 + + + + + + + 取消 + 自动合并所有 + + + + + + + + + diff --git a/pages/account-sync/phone-binding/phone-binding.wxss b/pages/account-sync/phone-binding/phone-binding.wxss new file mode 100644 index 0000000..b7209d1 --- /dev/null +++ b/pages/account-sync/phone-binding/phone-binding.wxss @@ -0,0 +1,384 @@ +/* 手机号绑定页面样式 */ +@import "../../../styles/design-system.wxss"; +@import "../../../styles/components.wxss"; + +.container { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* 自定义导航栏 */ +.custom-navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); +} + +.navbar-content { + display: flex; + align-items: center; + justify-content: space-between; + height: 44px; + padding: 0 32rpx; +} + +.navbar-left { + width: 80rpx; + display: flex; + align-items: center; + justify-content: flex-start; +} + +.back-icon { + font-size: 36rpx; + color: #333; + font-weight: bold; +} + +.navbar-title { + font-size: 34rpx; + font-weight: 600; + color: #333; +} + +.navbar-right { + width: 80rpx; +} + +/* 主要内容 */ +.main-content { + flex: 1; + padding: 120rpx 60rpx 40rpx; + display: flex; + flex-direction: column; +} + +/* 头部区域 */ +.header-section { + text-align: center; + margin-bottom: 80rpx; +} + +.header-icon { + font-size: 120rpx; + margin-bottom: 30rpx; +} + +.header-title { + font-size: 48rpx; + font-weight: bold; + color: white; + margin-bottom: 20rpx; +} + +.header-desc { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.8); + line-height: 1.6; + padding: 0 20rpx; +} + +/* 表单区域 */ +.form-section { + background: rgba(255, 255, 255, 0.95); + border-radius: 24rpx; + padding: 60rpx 40rpx; + margin-bottom: 60rpx; + box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.1); +} + +.input-group { + margin-bottom: 40rpx; +} + +.input-label { + display: block; + font-size: 28rpx; + color: #333; + margin-bottom: 16rpx; + font-weight: 500; +} + +.input-container { + position: relative; + display: flex; + align-items: center; + background: #f8f9fa; + border-radius: 16rpx; + border: 2rpx solid transparent; + transition: all 0.3s ease; +} + +.input-container:focus-within { + border-color: #667eea; + background: white; + box-shadow: 0 0 0 6rpx rgba(102, 126, 234, 0.1); +} + +.input-icon { + padding: 0 20rpx; + font-size: 32rpx; + color: #999; +} + +.phone-input, +.code-input { + flex: 1; + height: 88rpx; + font-size: 32rpx; + color: #333; + background: transparent; + border: none; + outline: none; +} + +.input-placeholder { + color: #999; +} + +/* 验证码容器 */ +.code-container { + padding-right: 0; +} + +.code-button { + height: 88rpx; + padding: 0 24rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0 16rpx 16rpx 0; + transition: all 0.3s ease; + border-left: 2rpx solid #eee; +} + +.code-button.active { + background: #667eea; + color: white; + cursor: pointer; +} + +.code-button.disabled { + background: #f5f5f5; + color: #999; +} + +.code-button-text { + font-size: 24rpx; + font-weight: 500; +} + +/* 绑定按钮 */ +.bind-button { + width: 100%; + height: 96rpx; + border-radius: 16rpx; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + margin-top: 20rpx; +} + +.bind-button.active { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3); +} + +.bind-button.disabled { + background: #e9ecef; +} + +.bind-button.active:active { + transform: scale(0.98); +} + +.bind-content { + display: flex; + align-items: center; + justify-content: center; + position: relative; + z-index: 2; +} + +.bind-icon { + font-size: 36rpx; + margin-right: 12rpx; +} + +.bind-text { + font-size: 32rpx; + font-weight: 500; +} + +.bind-button.active .bind-text { + color: white; +} + +.bind-button.disabled .bind-text { + color: #999; +} + +/* 温馨提示 */ +.tips-section { + background: rgba(255, 255, 255, 0.9); + border-radius: 16rpx; + padding: 40rpx; +} + +.tips-title { + font-size: 28rpx; + font-weight: 600; + color: #333; + margin-bottom: 20rpx; +} + +.tips-item { + font-size: 24rpx; + color: #666; + line-height: 1.6; + margin-bottom: 12rpx; +} + +/* 合并对话框 */ +.merge-dialog-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 2000; + padding: 40rpx; +} + +.merge-dialog { + background: white; + border-radius: 24rpx; + width: 100%; + max-width: 600rpx; + max-height: 80vh; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.dialog-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 40rpx 40rpx 20rpx; + border-bottom: 2rpx solid #f0f0f0; +} + +.dialog-title { + font-size: 32rpx; + font-weight: 600; + color: #333; +} + +.dialog-close { + font-size: 48rpx; + color: #999; + width: 60rpx; + height: 60rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.dialog-content { + flex: 1; + padding: 20rpx 40rpx; + overflow-y: auto; +} + +.dialog-desc { + font-size: 28rpx; + color: #666; + margin-bottom: 30rpx; + line-height: 1.5; +} + +.candidates-list { + margin-bottom: 20rpx; +} + +.candidate-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 30rpx 20rpx; + background: #f8f9fa; + border-radius: 16rpx; + margin-bottom: 20rpx; +} + +.candidate-info { + flex: 1; +} + +.candidate-name { + font-size: 30rpx; + font-weight: 600; + color: #333; + margin-bottom: 10rpx; +} + +.candidate-detail { + display: flex; + flex-direction: column; +} + +.detail-item { + font-size: 24rpx; + color: #666; + margin-bottom: 4rpx; +} + +.merge-btn { + background: #667eea; + color: white; + padding: 16rpx 24rpx; + border-radius: 12rpx; + font-size: 24rpx; + font-weight: 500; +} + +.dialog-actions { + display: flex; + padding: 20rpx 40rpx 40rpx; + gap: 20rpx; +} + +.action-btn { + flex: 1; + height: 80rpx; + border-radius: 12rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + font-weight: 500; +} + +.cancel-btn { + background: #f8f9fa; + color: #666; +} + +.auto-btn { + background: #667eea; + color: white; +} diff --git a/pages/chat-settings/chat-settings.js b/pages/chat-settings/chat-settings.js new file mode 100644 index 0000000..21a968e --- /dev/null +++ b/pages/chat-settings/chat-settings.js @@ -0,0 +1,469 @@ +// 🎨 聊天设置页面逻辑 +const app = getApp(); + +Page({ + data: { + // 系统信息 + statusBarHeight: 44, + navBarHeight: 88, + + // 当前设置 + currentBackground: 'default', + fontSize: 'medium', + showTimestamp: true, + showReadStatus: true, + bubbleStyle: 'classic', + bubbleStyleName: '经典样式', + enableReactions: true, + burnAfterReading: false, + endToEndEncryption: false, + + // 预设背景 + presetBackgrounds: [ + { + id: 'gradient1', + name: '渐变蓝', + thumbnail: '/images/backgrounds/gradient1-thumb.jpg', + url: '/images/backgrounds/gradient1.jpg' + }, + { + id: 'gradient2', + name: '渐变紫', + thumbnail: '/images/backgrounds/gradient2-thumb.jpg', + url: '/images/backgrounds/gradient2.jpg' + }, + { + id: 'nature1', + name: '自然风光', + thumbnail: '/images/backgrounds/nature1-thumb.jpg', + url: '/images/backgrounds/nature1.jpg' + }, + { + id: 'abstract1', + name: '抽象艺术', + thumbnail: '/images/backgrounds/abstract1-thumb.jpg', + url: '/images/backgrounds/abstract1.jpg' + } + ], + + // 气泡样式选项 + bubbleStyles: [ + { + id: 'classic', + name: '经典样式', + class: 'classic' + }, + { + id: 'modern', + name: '现代样式', + class: 'modern' + }, + { + id: 'minimal', + name: '简约样式', + class: 'minimal' + } + ], + + // 弹窗状态 + showBubbleModal: false + }, + + onLoad(options) { + console.log('🎨 聊天设置页面加载'); + + // 获取系统信息 + this.getSystemInfo(); + + // 加载用户设置 + this.loadUserSettings(); + }, + + onShow() { + console.log('🎨 聊天设置页面显示'); + }, + + // 获取系统信息 + getSystemInfo() { + const systemInfo = wx.getSystemInfoSync(); + this.setData({ + statusBarHeight: systemInfo.statusBarHeight || 44, + navBarHeight: 88 + }); + }, + + // 加载用户设置 + loadUserSettings() { + try { + const settings = wx.getStorageSync('chatSettings') || {}; + + this.setData({ + currentBackground: settings.background || 'default', + fontSize: settings.fontSize || 'medium', + showTimestamp: settings.showTimestamp !== false, + showReadStatus: settings.showReadStatus !== false, + bubbleStyle: settings.bubbleStyle || 'classic', + enableReactions: settings.enableReactions !== false, + burnAfterReading: settings.burnAfterReading || false, + endToEndEncryption: settings.endToEndEncryption || false + }); + + // 更新气泡样式名称 + this.updateBubbleStyleName(); + + console.log('✅ 用户设置加载完成'); + + } catch (error) { + console.error('❌ 加载用户设置失败:', error); + } + }, + + // 🎨 ===== 背景设置 ===== + + // 选择背景 + selectBackground(e) { + const type = e.currentTarget.dataset.type; + const id = e.currentTarget.dataset.id; + + console.log('🎨 选择背景:', type, id); + + let backgroundId = type; + if (type === 'preset') { + backgroundId = id; + } + + this.setData({ + currentBackground: backgroundId + }); + + // 立即应用背景 + this.applyBackground(backgroundId); + }, + + // 选择自定义背景 + selectCustomBackground() { + console.log('🎨 选择自定义背景'); + + wx.chooseImage({ + count: 1, + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success: (res) => { + const tempFilePath = res.tempFilePaths[0]; + + // 保存自定义背景 + this.saveCustomBackground(tempFilePath); + }, + fail: (error) => { + console.error('❌ 选择图片失败:', error); + wx.showToast({ + title: '选择图片失败', + icon: 'none' + }); + } + }); + }, + + // 保存自定义背景 + async saveCustomBackground(tempFilePath) { + try { + wx.showLoading({ + title: '设置背景中...' + }); + + // 保存图片到本地 + const savedFilePath = await new Promise((resolve, reject) => { + wx.saveFile({ + tempFilePath: tempFilePath, + success: (res) => resolve(res.savedFilePath), + fail: reject + }); + }); + + // 更新设置 + this.setData({ + currentBackground: 'custom' + }); + + // 保存自定义背景路径 + wx.setStorageSync('customBackground', savedFilePath); + + // 应用背景 + this.applyBackground('custom'); + + wx.hideLoading(); + wx.showToast({ + title: '背景设置成功', + icon: 'success' + }); + + } catch (error) { + wx.hideLoading(); + console.error('❌ 保存自定义背景失败:', error); + wx.showToast({ + title: '设置背景失败', + icon: 'none' + }); + } + }, + + // 应用背景 + applyBackground(backgroundId) { + try { + let backgroundUrl = ''; + + if (backgroundId === 'default') { + backgroundUrl = ''; + } else if (backgroundId === 'custom') { + backgroundUrl = wx.getStorageSync('customBackground') || ''; + } else { + // 预设背景 + const preset = this.data.presetBackgrounds.find(bg => bg.id === backgroundId); + if (preset) { + backgroundUrl = preset.url; + } + } + + // 保存到全局状态 + app.globalData.chatBackground = { + id: backgroundId, + url: backgroundUrl + }; + + console.log('✅ 背景应用成功:', backgroundId); + + } catch (error) { + console.error('❌ 应用背景失败:', error); + } + }, + + // 🔤 ===== 字体设置 ===== + + // 选择字体大小 + selectFontSize(e) { + const size = e.currentTarget.dataset.size; + console.log('🔤 选择字体大小:', size); + + this.setData({ + fontSize: size + }); + + // 立即应用字体大小 + this.applyFontSize(size); + }, + + // 应用字体大小 + applyFontSize(size) { + try { + // 保存到全局状态 + app.globalData.fontSize = size; + + console.log('✅ 字体大小应用成功:', size); + + } catch (error) { + console.error('❌ 应用字体大小失败:', error); + } + }, + + // ⚙️ ===== 消息设置 ===== + + // 时间戳设置变化 + onTimestampChange(e) { + const checked = e.detail.value; + console.log('⚙️ 时间戳设置变化:', checked); + + this.setData({ + showTimestamp: checked + }); + }, + + // 已读状态设置变化 + onReadStatusChange(e) { + const checked = e.detail.value; + console.log('⚙️ 已读状态设置变化:', checked); + + this.setData({ + showReadStatus: checked + }); + }, + + // 显示气泡样式选项 + showBubbleStyleOptions() { + console.log('🎨 显示气泡样式选项'); + + this.setData({ + showBubbleModal: true + }); + }, + + // 关闭气泡样式弹窗 + closeBubbleModal() { + this.setData({ + showBubbleModal: false + }); + }, + + // 选择气泡样式 + selectBubbleStyle(e) { + const styleId = e.currentTarget.dataset.id; + console.log('🎨 选择气泡样式:', styleId); + + this.setData({ + bubbleStyle: styleId + }); + + this.updateBubbleStyleName(); + this.closeBubbleModal(); + }, + + // 更新气泡样式名称 + updateBubbleStyleName() { + const style = this.data.bubbleStyles.find(s => s.id === this.data.bubbleStyle); + if (style) { + this.setData({ + bubbleStyleName: style.name + }); + } + }, + + // 表情回应设置变化 + onReactionsChange(e) { + const checked = e.detail.value; + console.log('⚙️ 表情回应设置变化:', checked); + + this.setData({ + enableReactions: checked + }); + }, + + // 🔒 ===== 隐私设置 ===== + + // 阅后即焚设置变化 + onBurnAfterReadingChange(e) { + const checked = e.detail.value; + console.log('🔒 阅后即焚设置变化:', checked); + + this.setData({ + burnAfterReading: checked + }); + }, + + // 加密设置变化 + onEncryptionChange(e) { + const checked = e.detail.value; + console.log('🔒 加密设置变化:', checked); + + this.setData({ + endToEndEncryption: checked + }); + }, + + // 💾 ===== 设置保存 ===== + + // 保存设置 + saveSettings() { + console.log('💾 保存设置'); + + try { + const settings = { + background: this.data.currentBackground, + fontSize: this.data.fontSize, + showTimestamp: this.data.showTimestamp, + showReadStatus: this.data.showReadStatus, + bubbleStyle: this.data.bubbleStyle, + enableReactions: this.data.enableReactions, + burnAfterReading: this.data.burnAfterReading, + endToEndEncryption: this.data.endToEndEncryption + }; + + // 保存到本地存储 + wx.setStorageSync('chatSettings', settings); + + // 应用到全局状态 + app.globalData.chatSettings = settings; + + wx.showToast({ + title: '设置保存成功', + icon: 'success' + }); + + console.log('✅ 设置保存成功'); + + } catch (error) { + console.error('❌ 保存设置失败:', error); + wx.showToast({ + title: '保存失败', + icon: 'none' + }); + } + }, + + // 恢复默认设置 + resetSettings() { + console.log('🔄 恢复默认设置'); + + wx.showModal({ + title: '恢复默认设置', + content: '确定要恢复所有设置到默认值吗?', + success: (res) => { + if (res.confirm) { + this.performReset(); + } + } + }); + }, + + // 执行重置 + performReset() { + try { + // 重置所有设置 + this.setData({ + currentBackground: 'default', + fontSize: 'medium', + showTimestamp: true, + showReadStatus: true, + bubbleStyle: 'classic', + enableReactions: true, + burnAfterReading: false, + endToEndEncryption: false + }); + + this.updateBubbleStyleName(); + + // 清除本地存储 + wx.removeStorageSync('chatSettings'); + wx.removeStorageSync('customBackground'); + + // 重置全局状态 + app.globalData.chatSettings = {}; + app.globalData.chatBackground = {}; + app.globalData.fontSize = 'medium'; + + wx.showToast({ + title: '已恢复默认设置', + icon: 'success' + }); + + console.log('✅ 默认设置恢复成功'); + + } catch (error) { + console.error('❌ 恢复默认设置失败:', error); + wx.showToast({ + title: '恢复失败', + icon: 'none' + }); + } + }, + + // 🧭 ===== 页面导航 ===== + + // 返回上一页 + goBack() { + wx.navigateBack(); + }, + + // 阻止事件冒泡 + stopPropagation() { + // 阻止点击事件冒泡 + } +}); diff --git a/pages/chat-settings/chat-settings.json b/pages/chat-settings/chat-settings.json new file mode 100644 index 0000000..5c144e6 --- /dev/null +++ b/pages/chat-settings/chat-settings.json @@ -0,0 +1,7 @@ +{ + "navigationStyle": "custom", + "backgroundColor": "#F2F2F7", + "backgroundTextStyle": "dark", + "enablePullDownRefresh": false, + "onReachBottomDistance": 50 +} diff --git a/pages/chat-settings/chat-settings.wxml b/pages/chat-settings/chat-settings.wxml new file mode 100644 index 0000000..e2cfe66 --- /dev/null +++ b/pages/chat-settings/chat-settings.wxml @@ -0,0 +1,217 @@ + + + + + + + + + + + 聊天设置 + + + + + + + + + + + + 聊天背景 + + + + + + + 默认 + + 默认背景 + + + + + + + {{item.name}} + + + + + + 📷 + + 自定义 + + + + + + + + 字体大小 + + + + + + + + + + + + + + + 特大 + + + + + + + 这是字体大小预览效果 + + + + + + + + + 消息设置 + + + + + + + 显示时间戳 + 在消息旁显示发送时间 + + + + + + + + 显示已读状态 + 显示消息的已读/未读状态 + + + + + + + + 消息气泡样式 + {{bubbleStyleName}} + + + + + + + + 表情回应 + 允许对消息添加表情回应 + + + + + + + + + + 隐私设置 + + + + + + + 阅后即焚 + 消息阅读后自动删除 + + + + + + + + 端到端加密 + 使用端到端加密保护消息 + + + + + + + + + + 保存设置 + + + + 恢复默认 + + + + + + + + + + 选择气泡样式 + + + + + + + + + 示例消息 + + {{item.name}} + + + + diff --git a/pages/chat-settings/chat-settings.wxss b/pages/chat-settings/chat-settings.wxss new file mode 100644 index 0000000..410dee5 --- /dev/null +++ b/pages/chat-settings/chat-settings.wxss @@ -0,0 +1,529 @@ +/* 🎨 聊天设置页面样式 */ + +/* CSS变量定义 */ +page { + --primary-color: #007AFF; + --primary-light: #5AC8FA; + --primary-dark: #0051D5; + --background-color: #F2F2F7; + --surface-color: #FFFFFF; + --text-primary: #000000; + --text-secondary: #8E8E93; + --text-tertiary: #C7C7CC; + --border-color: #E5E5EA; + --shadow-light: 0 1rpx 3rpx rgba(0, 0, 0, 0.1); + --shadow-medium: 0 4rpx 12rpx rgba(0, 0, 0, 0.15); + --radius-small: 8rpx; + --radius-medium: 12rpx; + --radius-large: 20rpx; +} + +/* 🌙 深色模式支持 */ +@media (prefers-color-scheme: dark) { + page { + --primary-color: #0A84FF; + --primary-light: #64D2FF; + --primary-dark: #0056CC; + --background-color: #000000; + --surface-color: #1C1C1E; + --text-primary: #FFFFFF; + --text-secondary: #8E8E93; + --text-tertiary: #48484A; + --border-color: #38383A; + --shadow-light: 0 1rpx 3rpx rgba(0, 0, 0, 0.3); + --shadow-medium: 0 4rpx 12rpx rgba(0, 0, 0, 0.4); + } +} + +.chat-settings-container { + height: 100vh; + background: var(--background-color); + display: flex; + flex-direction: column; +} + +/* 🎨 自定义导航栏 */ +.custom-navbar { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%); + box-shadow: var(--shadow-medium); + z-index: 1000; +} + +.navbar-content { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32rpx; +} + +.navbar-left, .navbar-right { + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-medium); + transition: all 0.3s ease; +} + +.navbar-left:active { + background: rgba(255, 255, 255, 0.2); + transform: scale(0.95); +} + +.back-icon { + font-size: 48rpx; + color: white; + font-weight: 300; +} + +.navbar-title { + flex: 1; + text-align: center; +} + +.title-text { + font-size: 36rpx; + font-weight: 600; + color: white; +} + +/* 🎨 设置内容 */ +.settings-content { + flex: 1; + padding: 32rpx; +} + +.settings-section { + margin-bottom: 48rpx; +} + +.section-title { + margin-bottom: 24rpx; +} + +.section-title .title-text { + font-size: 32rpx; + font-weight: 600; + color: var(--text-primary); +} + +/* 🎨 聊天背景设置 */ +.background-options { + display: flex; + flex-wrap: wrap; + gap: 24rpx; +} + +.background-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 16rpx; + transition: all 0.3s ease; +} + +.background-item:active { + transform: scale(0.95); +} + +.background-item.active .background-preview { + border: 4rpx solid var(--primary-color); + box-shadow: 0 0 0 4rpx rgba(0, 122, 255, 0.2); +} + +.background-preview { + width: 160rpx; + height: 120rpx; + border-radius: var(--radius-medium); + border: 2rpx solid var(--border-color); + background-size: cover; + background-position: center; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + overflow: hidden; +} + +.default-bg { + background: linear-gradient(135deg, #F2F2F7 0%, #E5E5EA 100%); +} + +.custom-bg { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%); +} + +.preview-text { + font-size: 24rpx; + color: var(--text-secondary); + font-weight: 500; +} + +.custom-icon { + font-size: 48rpx; + color: white; +} + +.background-name { + font-size: 26rpx; + color: var(--text-secondary); + text-align: center; +} + +/* 🎨 字体大小设置 */ +.font-size-setting { + background: var(--surface-color); + border-radius: var(--radius-medium); + padding: 32rpx; + border: 1rpx solid var(--border-color); +} + +.font-size-options { + display: flex; + gap: 16rpx; + margin-bottom: 32rpx; +} + +.font-size-item { + flex: 1; + height: 80rpx; + border-radius: var(--radius-medium); + border: 2rpx solid var(--border-color); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + background: var(--background-color); +} + +.font-size-item:active { + transform: scale(0.95); +} + +.font-size-item.active { + border-color: var(--primary-color); + background: rgba(0, 122, 255, 0.1); +} + +.size-text { + font-weight: 600; + color: var(--text-primary); +} + +.small-text { font-size: 24rpx; } +.medium-text { font-size: 28rpx; } +.large-text { font-size: 32rpx; } +.xlarge-text { font-size: 36rpx; } + +.font-preview { + padding: 24rpx; + background: var(--background-color); + border-radius: var(--radius-small); + border: 1rpx solid var(--border-color); +} + +.preview-message { + background: var(--primary-color); + border-radius: var(--radius-medium); + padding: 20rpx 24rpx; + max-width: 400rpx; +} + +.preview-text { + color: white; + line-height: 1.4; +} + +.small-font { font-size: 26rpx; } +.medium-font { font-size: 30rpx; } +.large-font { font-size: 34rpx; } +.xlarge-font { font-size: 38rpx; } + +/* 🎨 设置项 */ +.setting-items { + background: var(--surface-color); + border-radius: var(--radius-medium); + border: 1rpx solid var(--border-color); + overflow: hidden; +} + +.setting-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32rpx; + border-bottom: 1rpx solid var(--border-color); + transition: all 0.2s ease; +} + +.setting-item:last-child { + border-bottom: none; +} + +.setting-item:active { + background: var(--background-color); +} + +.item-info { + flex: 1; + min-width: 0; +} + +.item-title { + font-size: 32rpx; + font-weight: 500; + color: var(--text-primary); + display: block; + margin-bottom: 8rpx; +} + +.item-desc { + font-size: 26rpx; + color: var(--text-secondary); + line-height: 1.4; +} + +.setting-switch { + transform: scale(0.8); +} + +.item-arrow { + font-size: 32rpx; + color: var(--text-tertiary); + font-weight: 300; +} + +/* 🎨 操作按钮 */ +.action-buttons { + display: flex; + flex-direction: column; + gap: 24rpx; + margin-top: 48rpx; +} + +.action-btn { + height: 96rpx; + border-radius: var(--radius-medium); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + border: 2rpx solid transparent; +} + +.action-btn:active { + transform: scale(0.98); +} + +.action-btn.primary { + background: var(--primary-color); + box-shadow: var(--shadow-medium); +} + +.action-btn.primary:active { + background: var(--primary-dark); +} + +.action-btn.secondary { + background: var(--surface-color); + border-color: var(--border-color); +} + +.action-btn.secondary:active { + background: var(--background-color); +} + +.btn-text { + font-size: 32rpx; + font-weight: 600; +} + +.action-btn.primary .btn-text { + color: white; +} + +.action-btn.secondary .btn-text { + color: var(--text-primary); +} + +/* 🎨 气泡样式选择弹窗 */ +.bubble-style-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.3s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.modal-content { + width: 90%; + max-width: 600rpx; + background: var(--surface-color); + border-radius: var(--radius-large); + box-shadow: var(--shadow-medium); + animation: scaleIn 0.3s ease-out; + overflow: hidden; +} + +@keyframes scaleIn { + from { + transform: scale(0.8); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32rpx; + border-bottom: 1rpx solid var(--border-color); +} + +.modal-title { + font-size: 36rpx; + font-weight: 600; + color: var(--text-primary); +} + +.close-btn { + width: 64rpx; + height: 64rpx; + border-radius: 32rpx; + background: var(--background-color); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.close-btn:active { + background: var(--border-color); + transform: scale(0.9); +} + +.close-icon { + font-size: 28rpx; + color: var(--text-secondary); +} + +.bubble-options { + padding: 32rpx; + display: flex; + flex-wrap: wrap; + gap: 24rpx; +} + +.bubble-option { + display: flex; + flex-direction: column; + align-items: center; + gap: 16rpx; + transition: all 0.3s ease; +} + +.bubble-option:active { + transform: scale(0.95); +} + +.bubble-option.active .bubble-preview { + border: 4rpx solid var(--primary-color); + box-shadow: 0 0 0 4rpx rgba(0, 122, 255, 0.2); +} + +.bubble-preview { + padding: 16rpx 24rpx; + border-radius: var(--radius-medium); + border: 2rpx solid var(--border-color); + transition: all 0.3s ease; +} + +.bubble-preview.classic { + background: var(--primary-color); + border-radius: 24rpx 24rpx 24rpx 8rpx; +} + +.bubble-preview.modern { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%); + border-radius: var(--radius-medium); +} + +.bubble-preview.minimal { + background: var(--surface-color); + border: 2rpx solid var(--primary-color); + border-radius: var(--radius-small); +} + +.bubble-text { + font-size: 28rpx; + color: white; +} + +.bubble-preview.minimal .bubble-text { + color: var(--primary-color); +} + +.bubble-name { + font-size: 26rpx; + color: var(--text-secondary); + text-align: center; +} + +/* 📱 响应式设计 */ +@media screen and (max-width: 375px) { + .settings-content { + padding: 24rpx; + } + + .background-preview { + width: 120rpx; + height: 90rpx; + } + + .font-size-options { + gap: 12rpx; + } + + .font-size-item { + height: 64rpx; + } + + .setting-item { + padding: 24rpx; + } +} + +@media screen and (min-width: 414px) { + .settings-content { + padding: 40rpx; + } + + .background-preview { + width: 180rpx; + height: 135rpx; + } + + .font-size-item { + height: 96rpx; + } + + .setting-item { + padding: 40rpx; + } +} diff --git a/pages/edit/edit.js b/pages/edit/edit.js new file mode 100644 index 0000000..62a651a --- /dev/null +++ b/pages/edit/edit.js @@ -0,0 +1,622 @@ +const app = getApp(); +const config = require('../../config/config.js'); +const apiClient = require('../../utils/api-client.js'); +const imageCacheManager = require('../../utils/image-cache-manager.js'); + +Page({ + data: { + userInfo: { + avatar: '', + nickname: '未设置昵称', + customId: '123456789', + signature: '', + career: '', + education: '', + gender: '', + birthday: '', + hometown: '', + constellation: '', + height: '', + personalityType: '', + sleepHabit: '', + socialActivity: '' + }, + isEditingNickname: false, + isEditingSignature: false, + tempNickname: '', + tempSignature: '', + showConstellationPicker: false, + showPersonalityPicker: false, + showCareerPicker: false, + showEducationPicker: false, + showHometownPicker: false, + showBirthdayPicker: false, + showHeightPicker: false, + showGenderPicker: false, + showSleepHabitPicker: false, + showSocialActivityPicker: false, + constellations: ['水瓶座', '双鱼座', '白羊座', '金牛座', '双子座', '巨蟹座', '狮子座', '处女座', '天秤座', '天蝎座', '射手座', '摩羯座'], + personalityTypes: ['INTJ', 'INTP', 'ENTJ', 'INFP', 'ENTP', 'INFJ', 'ENFP', 'ENFJ', 'ISTJ', 'ISFJ', 'ISTP', 'ISFP', 'ESTJ', 'ESFJ', 'ESTP', 'ESFP'], + careers: ['初中生', '高中生', '大学生', '研究生', '留学生', '科研', '警察', '医生', '护士', '程序员', '老师', '化妆师', '摄影师', '音乐', '美术', '金融', '厨师', '工程师', '公务员', '互联网', '产品经理', '模特', '演员', '导演', '律师', '创业者', '其他'], + educations: ['北京大学', '清华大学', '复旦大学', '上海交通大学', '浙江大学', '南京大学', '武汉大学', '中山大学', '四川大学', '哈尔滨工业大学', '大专', '中专', '高职', '高中'], + genders: ['男', '女'], + sleepHabits: ['早起鸟儿', '夜猫子', '规律型', '深度睡眠追求者', '碎片化睡眠者', '失眠困扰者', '咖啡因敏感型', '数字戒断者', '运动调节型', '挑战打卡型', '鼾声监测者', '生物钟调节者', '社区分享型'], + socialActivities: ['内容创作者', '观察者', '吃瓜者', '潜水者', '机器人', '社群型用户', 'KOL', 'KOC', '普通用户', '算法依赖型用户', '事件驱动型用户', '季节性活跃用户', '社交维系型用户', '兴趣社群型用户', '职业网络型用户', '娱乐消遣型用户', '购物种草型用户', '互动型用户'], + selectedConstellation: '', + selectedPersonality: '', + selectedCareer: '', + selectedEducation: '', + selectedGender: '', + selectedHeight: 170, + selectedSleepHabit: '', + selectedSocialActivity: '', + searchCareerText: '', + searchEducationText: '', + filteredCareers: [], + filteredEducations: [], + provinces: [], + cities: [], + selectedProvince: '', + selectedCity: '', + selectedYear: '', + selectedMonth: '', + selectedDay: '', + years: [], + months: [], + days: [] + }, + + onLoad: function() { + this.loadUserData(); + this.initDatePicker(); + this.initLocationData(); + }, + + loadUserData: function() { + const userInfo = app.globalData.userInfo || {}; + this.setData({ + userInfo: { + avatar: userInfo.avatar || '', + nickname: userInfo.nickname || '未设置昵称', + customId: userInfo.customId || '123456789', + signature: userInfo.signature || '', + career: userInfo.career || '', + education: userInfo.education || '', + gender: userInfo.gender || '', + birthday: userInfo.birthday || '', + hometown: userInfo.hometown || '', + constellation: userInfo.constellation || '', + height: userInfo.height || '', + personalityType: userInfo.personalityType || '', + sleepHabit: userInfo.sleepHabit || '', + socialActivity: userInfo.socialActivity || '' + }, + tempNickname: userInfo.nickname || '未设置昵称', + tempSignature: userInfo.signature || '' + }); + }, + + // 头像相关功能 + changeAvatar: function() { + wx.showActionSheet({ + itemList: ['拍照', '从相册选择'], + success: (res) => { + const sourceType = res.tapIndex === 0 ? ['camera'] : ['album']; + this.chooseImage(sourceType); + } + }); + }, + + chooseImage: function(sourceType) { + wx.chooseImage({ + count: 1, + sizeType: ['compressed'], + sourceType: sourceType, + success: (res) => { + if (sourceType[0] === 'camera') { + this.setData({ + tempAvatarPath: res.tempFilePaths[0], + showCameraPreview: true + }); + } else { + this.uploadAvatar(res.tempFilePaths[0]); + } + } + }); + }, + + retakePhoto: function() { + this.setData({ showCameraPreview: false }); + this.chooseImage(['camera']); + }, + + usePhoto: function() { + this.uploadAvatar(this.data.tempAvatarPath); + this.setData({ showCameraPreview: false }); + }, + + uploadAvatar: async function(tempFilePath) { + try { + wx.showLoading({ title: '上传中...' }); + // 模拟上传头像 + await new Promise(resolve => setTimeout(resolve, 1000)); + const newAvatarUrl = tempFilePath; + + const userInfo = this.data.userInfo; + userInfo.avatar = newAvatarUrl; + this.setData({ userInfo }); + + // 更新全局用户信息 + if (app.globalData.userInfo) { + app.globalData.userInfo.avatar = newAvatarUrl; + } + + wx.hideLoading(); + wx.showToast({ title: '头像更新成功', icon: 'success' }); + } catch (error) { + wx.hideLoading(); + wx.showToast({ title: '上传失败', icon: 'none' }); + } + }, + + // 昵称编辑 + startEditNickname: function() { + this.setData({ + isEditingNickname: true, + tempNickname: this.data.userInfo.nickname + }); + }, + + confirmEditNickname: function() { + if (this.data.tempNickname.length > 30) { + wx.showToast({ title: '昵称不能超过30字节', icon: 'none' }); + return; + } + + const userInfo = this.data.userInfo; + userInfo.nickname = this.data.tempNickname; + this.setData({ + userInfo: userInfo, + isEditingNickname: false + }); + + // 更新全局用户信息 + if (app.globalData.userInfo) { + app.globalData.userInfo.nickname = this.data.tempNickname; + } + }, + + cancelEditNickname: function() { + this.setData({ isEditingNickname: false }); + }, + + // 签名编辑 + startEditSignature: function() { + this.setData({ + isEditingSignature: true, + tempSignature: this.data.userInfo.signature + }); + }, + + confirmEditSignature: function() { + if (this.data.tempSignature.length > 200) { + wx.showToast({ title: '简介不能超过200字节', icon: 'none' }); + return; + } + + const userInfo = this.data.userInfo; + userInfo.signature = this.data.tempSignature; + this.setData({ + userInfo: userInfo, + isEditingSignature: false + }); + + // 更新全局用户信息 + if (app.globalData.userInfo) { + app.globalData.userInfo.signature = this.data.tempSignature; + } + }, + + cancelEditSignature: function() { + this.setData({ isEditingSignature: false }); + }, + + // 星座选择 + openConstellationPicker: function() { + this.setData({ + showConstellationPicker: true, + selectedConstellation: this.data.userInfo.constellation + }); + }, + + selectConstellation: function(e) { + const constellation = e.currentTarget.dataset.value; + this.setData({ + selectedConstellation: constellation + }); + }, + + confirmConstellation: function() { + const userInfo = this.data.userInfo; + userInfo.constellation = this.data.selectedConstellation; + this.setData({ + userInfo: userInfo, + showConstellationPicker: false + }); + }, + + // 人格类型选择 + openPersonalityPicker: function() { + this.setData({ + showPersonalityPicker: true, + selectedPersonality: this.data.userInfo.personalityType + }); + }, + + selectPersonality: function(e) { + const personality = e.currentTarget.dataset.value; + this.setData({ + selectedPersonality: personality + }); + }, + + confirmPersonality: function() { + const userInfo = this.data.userInfo; + userInfo.personalityType = this.data.selectedPersonality; + this.setData({ + userInfo: userInfo, + showPersonalityPicker: false + }); + }, + + // 职业选择 + openCareerPicker: function() { + this.setData({ + showCareerPicker: true, + searchCareerText: '', + filteredCareers: this.data.careers + }); + }, + + searchCareer: function(e) { + const text = e.detail.value; + const filtered = this.data.careers.filter(career => + career.includes(text) + ); + this.setData({ + searchCareerText: text, + filteredCareers: filtered + }); + }, + + selectCareer: function(e) { + const career = e.currentTarget.dataset.value; + const userInfo = this.data.userInfo; + userInfo.career = career; + this.setData({ + userInfo: userInfo, + showCareerPicker: false + }); + }, + + // 教育背景选择 + openEducationPicker: function() { + this.setData({ + showEducationPicker: true, + searchEducationText: '', + filteredEducations: this.data.educations + }); + }, + + searchEducation: function(e) { + const text = e.detail.value; + const filtered = this.data.educations.filter(edu => + edu.includes(text) + ); + this.setData({ + searchEducationText: text, + filteredEducations: filtered + }); + }, + + selectEducation: function(e) { + const education = e.currentTarget.dataset.value; + const userInfo = this.data.userInfo; + userInfo.education = education; + this.setData({ + userInfo: userInfo, + showEducationPicker: false + }); + }, + + // 家乡选择 + openHometownPicker: function() { + this.setData({ + showHometownPicker: true, + selectedProvince: this.data.userInfo.hometown ? this.data.userInfo.hometown.split(' ')[0] : '', + selectedCity: this.data.userInfo.hometown ? this.data.userInfo.hometown.split(' ')[1] : '' + }); + }, + + confirmHometown: function() { + const userInfo = this.data.userInfo; + userInfo.hometown = `${this.data.selectedProvince} ${this.data.selectedCity}`; + this.setData({ + userInfo: userInfo, + showHometownPicker: false + }); + }, + + // 生日选择 + openBirthdayPicker: function() { + this.setData({ + showBirthdayPicker: true + }); + }, + + confirmBirthday: function() { + const userInfo = this.data.userInfo; + userInfo.birthday = `${this.data.selectedYear}-${this.data.selectedMonth.toString().padStart(2, '0')}-${this.data.selectedDay.toString().padStart(2, '0')}`; + this.setData({ + userInfo: userInfo, + showBirthdayPicker: false + }); + }, + + // 身高选择 + openHeightPicker: function() { + this.setData({ + showHeightPicker: true, + selectedHeight: this.data.userInfo.height ? parseInt(this.data.userInfo.height) : 170 + }); + }, + + // 性别选择 + openGenderPicker: function() { + this.setData({ + showGenderPicker: true, + selectedGender: this.data.userInfo.gender + }); + }, + + // 睡眠习惯选择 + openSleepHabitPicker: function() { + this.setData({ + showSleepHabitPicker: true, + selectedSleepHabit: this.data.userInfo.sleepHabit + }); + }, + + // 社交活跃度选择 + openSocialActivityPicker: function() { + this.setData({ + showSocialActivityPicker: true, + selectedSocialActivity: this.data.userInfo.socialActivity + }); + }, + + // 初始化位置数据 + initLocationData: function() { + // 模拟省市数据 + this.setData({ + provinces: ['北京市', '上海市', '广东省', '江苏省', '浙江省'], + cities: { + '北京市': ['北京市'], + '上海市': ['上海市'], + '广东省': ['广州市', '深圳市', '珠海市'], + '江苏省': ['南京市', '苏州市', '无锡市'], + '浙江省': ['杭州市', '宁波市', '温州市'] + }, + selectedProvince: this.data.userInfo.hometown ? this.data.userInfo.hometown.split(' ')[0] : '', + selectedCity: this.data.userInfo.hometown ? this.data.userInfo.hometown.split(' ')[1] : '', + hometownValue: [0, 0] // 默认选中第一项 + }); + }, + + // 家乡选择变化处理 + onHometownChange: function(e) { + const value = e.detail.value; + const province = this.data.provinces[value[0]]; + const city = this.data.cities[province][value[1]]; + this.setData({ + selectedProvince: province, + selectedCity: city, + hometownValue: value + }); + }, + + initDatePicker: function() { + const years = []; + const currentYear = new Date().getFullYear(); + for (let i = currentYear; i >= 1950; i--) { + years.push(i); + } + + const months = []; + for (let i = 1; i <= 12; i++) { + months.push(i); + } + + const days = []; + for (let i = 1; i <= 31; i++) { + days.push(i); + } + + // 设置默认日期 + let defaultYear = currentYear - 20; + let defaultMonth = 1; + let defaultDay = 1; + + if (this.data.userInfo.birthday) { + const parts = this.data.userInfo.birthday.split('-'); + if (parts.length === 3) { + defaultYear = parseInt(parts[0]); + defaultMonth = parseInt(parts[1]); + defaultDay = parseInt(parts[2]); + } + } + + // 计算默认值的索引 + const yearIndex = years.indexOf(defaultYear); + const monthIndex = months.indexOf(defaultMonth); + const dayIndex = days.indexOf(defaultDay); + + this.setData({ + years: years, + months: months, + days: days, + selectedYear: defaultYear, + selectedMonth: defaultMonth, + selectedDay: defaultDay, + birthdayValue: [yearIndex, monthIndex, dayIndex] + }); + }, + + // 生日选择变化处理 + onBirthdayChange: function(e) { + const value = e.detail.value; + const year = this.data.years[value[0]]; + const month = this.data.months[value[1]]; + const day = this.data.days[value[2]]; + this.setData({ + selectedYear: year, + selectedMonth: month, + selectedDay: day, + birthdayValue: value + }); + }, + + + + bindProvinceChange: function(e) { + const province = this.data.provinces[e.detail.value]; + this.setData({ + selectedProvince: province, + selectedCity: this.data.cities[province][0] + }); + }, + + bindCityChange: function(e) { + this.setData({ + selectedCity: this.data.cities[this.data.selectedProvince][e.detail.value] + }); + }, + + bindYearChange: function(e) { + this.setData({ + selectedYear: this.data.years[e.detail.value] + }); + }, + + bindMonthChange: function(e) { + this.setData({ + selectedMonth: this.data.months[e.detail.value] + }); + }, + + bindDayChange: function(e) { + this.setData({ + selectedDay: this.data.days[e.detail.value] + }); + }, + + + + adjustHeight: function(e) { + this.setData({ + selectedHeight: e.detail.value + }); + }, + + selectGender: function(e) { + const gender = e.currentTarget.dataset.value; + const userInfo = this.data.userInfo; + userInfo.gender = gender; + this.setData({ + userInfo: userInfo, + showHeightGenderPicker: false + }); + }, + + confirmHeight: function() { + const userInfo = this.data.userInfo; + userInfo.height = this.data.selectedHeight; + this.setData({ + userInfo: userInfo, + showHeightPicker: false + }); + }, + + // 睡眠习惯选择处理 + // 社交活跃度选择处理 + + selectSleepHabit: function(e) { + const sleepHabit = e.currentTarget.dataset.value; + const userInfo = this.data.userInfo; + userInfo.sleepHabit = sleepHabit; + this.setData({ + userInfo: userInfo, + showSleepHabitPicker: false + }); + }, + + selectSocialActivity: function(e) { + const socialActivity = e.currentTarget.dataset.value; + const userInfo = this.data.userInfo; + userInfo.socialActivity = socialActivity; + this.setData({ + userInfo: userInfo, + showSocialActivityPicker: false + }); + }, + + // 身高选择确认 + confirmHeight: function() { + const userInfo = this.data.userInfo; + userInfo.height = this.data.selectedHeight; + this.setData({ + userInfo: userInfo, + showHeightPicker: false + }); + }, + + // 性别选择确认 + selectGender: function(e) { + const gender = e.currentTarget.dataset.value; + const userInfo = this.data.userInfo; + userInfo.gender = gender; + this.setData({ + userInfo: userInfo, + showGenderPicker: false + }); + }, + + // 关闭所有弹出层 + closeAllPickers: function() { + this.setData({ + showConstellationPicker: false, + showPersonalityPicker: false, + showCareerPicker: false, + showEducationPicker: false, + showHometownPicker: false, + showBirthdayPicker: false, + showHeightPicker: false, + showGenderPicker: false, + showSleepHabitPicker: false, + showSocialActivityPicker: false, + showCameraPreview: false + }); + }, + + // 保存所有修改 + saveChanges: function() { + // 这里可以添加保存到服务器的逻辑 + wx.showLoading({ title: '保存中...' }); + setTimeout(() => { + wx.hideLoading(); + wx.showToast({ title: '资料保存成功', icon: 'success' }); + // 返回个人资料页面 + wx.navigateBack(); + }, 1000); + } +}); \ No newline at end of file diff --git a/pages/edit/edit.json b/pages/edit/edit.json new file mode 100644 index 0000000..c8e97b4 --- /dev/null +++ b/pages/edit/edit.json @@ -0,0 +1,7 @@ +{ + "navigationBarTitleText": "编辑资料", + "navigationBarBackgroundColor": "#000000", + "navigationBarTextStyle": "white", + "backgroundColor": "#000000", + "disableScroll": true +} \ No newline at end of file diff --git a/pages/edit/edit.wxml b/pages/edit/edit.wxml new file mode 100644 index 0000000..1380078 --- /dev/null +++ b/pages/edit/edit.wxml @@ -0,0 +1,310 @@ + + + + + + + 保存 + + + + + + + + + + + + + + + + + + + + + 用户ID + {{userInfo.customId}} + + + + + 昵称 + + {{userInfo.nickname}} + + + + + + + + 个人简历 + + {{userInfo.signature || '点击添加个人简历'}} + + + + + + + 关于我 + + + + 职业 + + {{userInfo.career || ' '}} + + + + + + + 教育 + + {{userInfo.education || ' '}} + + + + + + + + + 性别 + {{userInfo.gender || ' '}} + + + + + + 生日 + {{userInfo.birthday || ' '}} + + + + + + 家乡 + {{userInfo.hometown || ' '}} + + + + + + + + 更多 + + + + 星座 + + {{userInfo.constellation || ' '}} + + + + + + + 身高 + + {{userInfo.height ? userInfo.height + 'cm' : ' '}} + + + + + + + 人格类型 + + {{userInfo.personalityType || ' '}} + + + + + + + 睡眠习惯 + + {{userInfo.sleepHabit || ' '}} + + + + + + + 社交活跃度 + + {{userInfo.socialActivity || ' '}} + + + + + + + + + 编辑昵称 + + + 取消 + 确定 + + + + + + + 编辑个人简介 + + + 取消 + 确定 + + + + + + + 选择星座 + + {{item}} + + 确定 + + + + + + 选择人格类型 + + {{item}} + + 确定 + + + + + + 选择职业 + + + 🔍 + + + {{item}} + + + + + + + 选择教育背景 + + + 🔍 + + + {{item}} + + + + + + + 选择家乡 + + + + {{item}} + + + {{item}} + + + + 确定 + + + + + + 选择生日 + + + + {{item}} + + + {{item}}月 + + + {{item}}日 + + + + 确定 + + + + + + 选择身高 + + 身高 + + {{selectedHeight}}cm + + 确定 + + + + + + 选择性别 + + 性别 + + + + + + + + + + + 选择睡眠习惯 + + + {{item}} + + + + + + + + 选择社交活跃度 + + + + + + + + + + + 重拍 + 使用照片 + + + \ No newline at end of file diff --git a/pages/edit/edit.wxss b/pages/edit/edit.wxss new file mode 100644 index 0000000..ba9aeb0 --- /dev/null +++ b/pages/edit/edit.wxss @@ -0,0 +1,651 @@ +.profile-edit-container { + min-height: 100vh; + background: #000000; + color: #ffffff; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + padding-bottom: 60rpx; +} + +/* 滑动容器样式 */ +.scroll-container { + height: calc(100vh - 88rpx); + overflow-y: auto; + padding-bottom: 60rpx; +} + +/* 导航栏样式 */ +.nav-bar { + display: flex; + justify-content: space-between; + align-items: center; + height: 88rpx; + padding: 0 30rpx; + /* background-color: #6e0000; */ + position: sticky; + top: 0; + z-index: 100; +} + +.nav-back { + width: 88rpx; + height: 88rpx; + display: flex; + align-items: center; + justify-content: flex-start; +} + +.back-icon { + font-size: 40rpx; + color: #ffffff; +} + +.nav-title { + font-size: 36rpx; + font-weight: 600; + color: #ffffff; +} + +.nav-save { + width: 88rpx; + height: 88rpx; + display: flex; + align-items: center; + justify-content: flex-end; +} + +.save-text { + font-size: 32rpx; + color: #07c160; + font-weight: 500; +} + +/* 头像区域样式 */ +.avatar-section { + display: flex; + justify-content: center; + padding: 60rpx 0; +} + +.avatar-container { + position: relative; + width: 200rpx; + height: 200rpx; + border-radius: 50%; + overflow: hidden; + border: 4rpx solid #333333; +} + +.avatar { + width: 100%; + height: 100%; +} + +.avatar-upload { + position: absolute; + bottom: 0; + right: 0; + width: 60rpx; + height: 60rpx; + background-color: #555455; + border-radius: 100%; + display: flex; + align-items: center; + justify-content: center; + border: 4rpx solid #1a1a1a; +} + +.upload-icon { + font-size: 36rpx; + color: #ffffff; + font-weight: bold; +} + +/* 信息区域样式 */ +.info-section { + margin: 0 40rpx 50rpx; + background-color: #242424; + border-radius: 20rpx; + padding: 40rpx; +} + +.info-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 30rpx 0; + border-bottom: 2rpx solid #333333; +} + +.info-item:last-child { + border-bottom: none; +} + +.info-label { + font-size: 32rpx; + color: #cccccc; + width: 140rpx; +} + +.info-value { + font-size: 32rpx; + color: #ffffff; + flex: 1; + text-align: right; +} + +.nickname-container, +.signature-container { + display: flex; + justify-content: flex-end; + align-items: center; + flex: 1; +} + +.edit-icon { + font-size: 28rpx; + color: #07c160; + margin-left: 10rpx; +} + +.signature { + text-align: right; + color: #999999; +} + +/* 关于我区域样式 */ +.about-section { + padding: 40rpx 32rpx; + margin: 0 40rpx 50rpx; + background-color: #242424; + border-radius: 24rpx; +} + +.resume-section { + margin: 0 40rpx 50rpx; + padding: 40rpx; + background-color: #242424; + border-radius: 20rpx; +} + +.resume-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 30rpx 0; +} + +.resume-value { + color: #f5f5f5; + font-size: 28rpx; + line-height: 1.5; +} + +/* 更多 */ +.more-section { + margin: 0 40rpx 50rpx; + background-color: #242424; + border-radius: 20rpx; + padding: 40rpx; +} + +.section-title { + font-size: 34rpx; + font-weight: 600; + margin-bottom: 30rpx; + color: #ffffff; +} + +.section-subtitle { + font-size: 30rpx; + color: #cccccc; + margin: 20rpx 0; +} + +.about-item, +.more-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 30rpx 0; + border-bottom: 2rpx solid #333333; +} + +.about-item:last-child, +.more-item:last-child { + border-bottom: none; +} + +.about-label, +.more-label { + font-size: 32rpx; + color: #cccccc; + width: 160rpx; +} + +.about-content, +.more-content { + display: flex; + justify-content: flex-end; + align-items: center; + flex: 1; +} + +.about-value, +.more-value { + font-size: 32rpx; + color: linear-gradient(123deg, #8361FB 15.54%, #70AAFC 39.58%, #F0F8FB 62.43%, #F07BFF 90.28%); + text-align: right; + margin-right: 10rpx; +} + +.arrow-icon { + font-size: 32rpx; + color: #666666; +} + +.basic-info { + margin-top: 30rpx; +} + +.basic-info-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 30rpx 0; + border-bottom: 2rpx solid #333333; +} + +.basic-info-item:last-child { + border-bottom: none; +} + +/* 弹窗样式 */ +.modal-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; +} + +.modal-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: #242424; + border-radius: 30rpx 30rpx 0 0; + padding: 30rpx; + z-index: 1001; +} + +.modal-title { + font-size: 36rpx; + font-weight: 600; + text-align: center; + margin-bottom: 30rpx; + color: #ffffff; +} + +.modal-input { + width: 100%; + height: 80rpx; + background-color: #333333; + border-radius: 10rpx; + padding: 0 20rpx; + font-size: 32rpx; + color: #ffffff; + margin-bottom: 30rpx; +} + +.modal-textarea { + width: 100%; + height: 200rpx; + background-color: #333333; + border-radius: 10rpx; + padding: 20rpx; + font-size: 32rpx; + color: #ffffff; + margin-bottom: 30rpx; + line-height: 1.5; +} + +.modal-buttons { + display: flex; + justify-content: space-between; +} + +.modal-button { + width: 300rpx; + height: 80rpx; + border-radius: 40rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + font-weight: 500; +} + +.cancel { + background-color: #333333; + color: #999999; +} + +.confirm { + background-color: #07c160; + color: #ffffff; +} + +/* 选择器样式 */ +.picker-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; +} + +.picker-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: #242424; + border-radius: 30rpx 30rpx 0 0; + padding: 30rpx; + z-index: 1001; + max-height: 80vh; + overflow-y: auto; +} + +.picker-title { + font-size: 36rpx; + font-weight: 600; + text-align: center; + margin-bottom: 30rpx; + color: #ffffff; +} + +.picker-button { + width: 100%; + height: 80rpx; + border-radius: 40rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + font-weight: 500; + margin-top: 30rpx; +} + +/* 星座选择器样式 */ +.constellation-grid { + display: flex; + flex-wrap: wrap; + justify-content: space-between; +} + +.constellation-item { + width: 140rpx; + height: 60rpx; + background-color: #333333; + border-radius: 30rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + color: #ffffff; + margin-bottom: 20rpx; +} + +.constellation-item.selected { + background-color: #07c160; +} + +/* 人格类型选择器样式 */ +.personality-grid { + display: flex; + flex-wrap: wrap; + justify-content: space-between; +} + +.personality-item { + width: 140rpx; + height: 60rpx; + background-color: #333333; + border-radius: 30rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + color: #ffffff; + margin-bottom: 20rpx; +} + +.personality-item.selected { + background-color: #07c160; +} + +/* 职业和教育选择器样式 */ +.search-container { + display: flex; + align-items: center; + background-color: #333333; + border-radius: 10rpx; + padding: 0 20rpx; + margin-bottom: 20rpx; +} + +.search-input { + flex: 1; + height: 70rpx; + font-size: 30rpx; + color: #ffffff; +} + +.search-icon { + font-size: 32rpx; + color: #999999; +} + +.career-scroll, +.education-scroll { + height: 400rpx; +} + +.career-item, +.education-item { + height: 80rpx; + display: flex; + align-items: center; + padding: 0 20rpx; + border-bottom: 2rpx solid #333333; + font-size: 30rpx; + color: #ffffff; +} + +/* 家乡和生日选择器样式 */ +.hometown-picker-view, +.birthday-picker-view { + background-color: #333333; + border-radius: 16rpx; + padding: 20rpx; + margin-bottom: 30rpx; +} + +.birthday-picker-view picker-view { + border-radius: 12rpx; + overflow: hidden; +} + +.birthday-picker-view picker-view-column view { + font-size: 32rpx; + color: #ffffff; + text-align: center; +} + +.birthday-picker-view picker-view ::-webkit-scrollbar { + width: 0; + height: 0; +} + +.hometown-picker, +.birthday-picker { + margin-bottom: 30rpx; +} + +.picker-label { + font-size: 32rpx; + color: #cccccc; + margin-bottom: 10rpx; + display: block; +} + +.picker-text { + height: 70rpx; + background-color: #333333; + border-radius: 10rpx; + display: flex; + align-items: center; + padding: 0 20rpx; + font-size: 30rpx; + color: #ffffff; + margin-bottom: 20rpx; +} + +.birthday-picker { + display: flex; + flex-wrap: wrap; + justify-content: space-between; +} + +.birthday-picker .picker { + width: 30%; +} + +/* 身高和性别选择器样式 */ +.height-picker, +.gender-picker { + margin-bottom: 30rpx; +} + +.slider { + width: 100%; + margin: 20rpx 0; +} + +.height-value { + font-size: 32rpx; + color: #07c160; + text-align: center; + margin-top: 10rpx; +} + +.gender-options { + display: flex; + justify-content: space-around; + margin-top: 20rpx; +} + +.gender-option { + width: 150rpx; + height: 70rpx; + background-color: #333333; + border-radius: 35rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + color: #ffffff; +} + +.gender-option.selected { + background-color: #07c160; +} + +/* 睡眠习惯和社交活跃度选择器样式 */ +.sleep-habit-section, +.social-activity-section { + margin-bottom: 30rpx; +} + +.sleep-habit-scroll, +.social-activity-scroll { + height: 250rpx; +} + +.habit-item, +.activity-item { + height: 70rpx; + display: flex; + align-items: center; + padding: 0 20rpx; + border-bottom: 2rpx solid #333333; + font-size: 30rpx; + color: #ffffff; +} + +/* 相机预览样式 */ +.camera-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.8); + z-index: 2000; +} + +.camera-preview { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 2001; + display: flex; + flex-direction: column; + align-items: center; +} + +.camera-preview image { + width: 600rpx; + height: 600rpx; + object-fit: contain; +} + +.preview-buttons { + display: flex; + justify-content: space-between; + width: 600rpx; + margin-top: 40rpx; +} + +.preview-button { + width: 250rpx; + height: 80rpx; + border-radius: 40rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + font-weight: 500; +} + +.retake { + background-color: #333333; + color: #999999; +} + +.use { + background-color: #07c160; + color: #ffffff; +} + +/* 适配底部安全区域 */ +.bottom-space { + height: 34rpx; +} \ No newline at end of file diff --git a/pages/group/create-group/create-group.js b/pages/group/create-group/create-group.js new file mode 100644 index 0000000..3cf416d --- /dev/null +++ b/pages/group/create-group/create-group.js @@ -0,0 +1,374 @@ +// 👥 创建群聊页面逻辑 +const app = getApp(); +const groupChatManager = require('../../../utils/group-chat-manager.js'); +const apiClient = require('../../../utils/api-client.js'); + +Page({ + data: { + // 系统信息 + statusBarHeight: 44, + navBarHeight: 88, + + // 群信息 + groupName: '', + groupDescription: '', + groupAvatar: '', + + // 配置限制 + maxGroupNameLength: 20, + maxDescriptionLength: 200, + + // 成员选择 + friendsList: [], + filteredFriends: [], + selectedMembers: [], + searchKeyword: '', + + // 群设置 + allowMemberInvite: true, + saveToContacts: true, + showQRCode: true, + + // 状态 + isCreating: false, + canCreate: false + }, + + onLoad(options) { + console.log('👥 创建群聊页面加载'); + + // 获取系统信息 + this.getSystemInfo(); + + // 加载好友列表 + this.loadFriendsList(); + + // 处理预选成员 + if (options.preSelectedMembers) { + try { + const preSelected = JSON.parse(decodeURIComponent(options.preSelectedMembers)); + this.setData({ + selectedMembers: preSelected + }); + this.updateCanCreate(); + } catch (error) { + console.error('❌ 解析预选成员失败:', error); + } + } + }, + + onShow() { + console.log('👥 创建群聊页面显示'); + }, + + // 获取系统信息 + getSystemInfo() { + const systemInfo = wx.getSystemInfoSync(); + this.setData({ + statusBarHeight: systemInfo.statusBarHeight || 44, + navBarHeight: 88 + }); + }, + + // 加载好友列表 + async loadFriendsList() { + try { + wx.showLoading({ + title: '加载好友列表...' + }); + + const response = await apiClient.request({ + url: '/api/v1/friends', + method: 'GET' + }); + + wx.hideLoading(); + + if (response.success) { + const friends = response.data || []; + + // 标记已选择的好友 + const friendsWithSelection = friends.map(friend => ({ + ...friend, + selected: this.data.selectedMembers.some(member => member.userId === friend.userId) + })); + + this.setData({ + friendsList: friendsWithSelection, + filteredFriends: friendsWithSelection + }); + + console.log('✅ 好友列表加载完成:', friends.length); + } else { + throw new Error(response.error || '加载好友列表失败'); + } + + } catch (error) { + wx.hideLoading(); + console.error('❌ 加载好友列表失败:', error); + wx.showToast({ + title: '加载好友列表失败', + icon: 'none' + }); + } + }, + + // 👥 ===== 群信息设置 ===== + + // 选择群头像 + selectGroupAvatar() { + console.log('👥 选择群头像'); + + wx.chooseImage({ + count: 1, + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success: (res) => { + const tempFilePath = res.tempFilePaths[0]; + this.setData({ + groupAvatar: tempFilePath + }); + console.log('✅ 群头像选择成功'); + }, + fail: (error) => { + console.error('❌ 选择群头像失败:', error); + wx.showToast({ + title: '选择头像失败', + icon: 'none' + }); + } + }); + }, + + // 群名称输入 + onGroupNameInput(e) { + const value = e.detail.value; + this.setData({ + groupName: value + }); + this.updateCanCreate(); + }, + + // 群描述输入 + onGroupDescInput(e) { + const value = e.detail.value; + this.setData({ + groupDescription: value + }); + }, + + // 👤 ===== 成员选择 ===== + + // 搜索输入 + onSearchInput(e) { + const keyword = e.detail.value; + this.setData({ + searchKeyword: keyword + }); + this.filterFriends(keyword); + }, + + // 清除搜索 + clearSearch() { + this.setData({ + searchKeyword: '' + }); + this.filterFriends(''); + }, + + // 过滤好友 + filterFriends(keyword) { + let filtered = this.data.friendsList; + + if (keyword.trim()) { + filtered = this.data.friendsList.filter(friend => { + const name = (friend.nickname || friend.username || '').toLowerCase(); + return name.includes(keyword.toLowerCase()); + }); + } + + this.setData({ + filteredFriends: filtered + }); + }, + + // 切换成员选择 + toggleMember(e) { + const userId = e.currentTarget.dataset.userId; + const friend = this.data.friendsList.find(f => f.userId === userId); + + if (!friend) return; + + console.log('👤 切换成员选择:', friend.nickname || friend.username); + + let selectedMembers = [...this.data.selectedMembers]; + let friendsList = [...this.data.friendsList]; + let filteredFriends = [...this.data.filteredFriends]; + + const isSelected = selectedMembers.some(member => member.userId === userId); + + if (isSelected) { + // 移除选择 + selectedMembers = selectedMembers.filter(member => member.userId !== userId); + } else { + // 添加选择 + selectedMembers.push(friend); + } + + // 更新好友列表的选择状态 + friendsList = friendsList.map(f => ({ + ...f, + selected: selectedMembers.some(member => member.userId === f.userId) + })); + + filteredFriends = filteredFriends.map(f => ({ + ...f, + selected: selectedMembers.some(member => member.userId === f.userId) + })); + + this.setData({ + selectedMembers: selectedMembers, + friendsList: friendsList, + filteredFriends: filteredFriends + }); + + this.updateCanCreate(); + }, + + // 移除成员 + removeMember(e) { + const userId = e.currentTarget.dataset.userId; + console.log('👤 移除成员:', userId); + + let selectedMembers = this.data.selectedMembers.filter(member => member.userId !== userId); + let friendsList = [...this.data.friendsList]; + let filteredFriends = [...this.data.filteredFriends]; + + // 更新好友列表的选择状态 + friendsList = friendsList.map(f => ({ + ...f, + selected: selectedMembers.some(member => member.userId === f.userId) + })); + + filteredFriends = filteredFriends.map(f => ({ + ...f, + selected: selectedMembers.some(member => member.userId === f.userId) + })); + + this.setData({ + selectedMembers: selectedMembers, + friendsList: friendsList, + filteredFriends: filteredFriends + }); + + this.updateCanCreate(); + }, + + // ⚙️ ===== 群设置 ===== + + // 允许成员邀请设置变化 + onAllowInviteChange(e) { + this.setData({ + allowMemberInvite: e.detail.value + }); + }, + + // 保存到通讯录设置变化 + onSaveContactsChange(e) { + this.setData({ + saveToContacts: e.detail.value + }); + }, + + // 显示群二维码设置变化 + onShowQRCodeChange(e) { + this.setData({ + showQRCode: e.detail.value + }); + }, + + // 🎯 ===== 群聊创建 ===== + + // 更新是否可以创建 + updateCanCreate() { + const canCreate = this.data.groupName.trim().length > 0 && this.data.selectedMembers.length > 0; + this.setData({ + canCreate: canCreate + }); + }, + + // 创建群聊 + async createGroup() { + if (!this.data.canCreate || this.data.isCreating) { + return; + } + + console.log('👥 开始创建群聊'); + + try { + this.setData({ + isCreating: true + }); + + // 构建群聊信息 + const groupInfo = { + name: this.data.groupName.trim(), + description: this.data.groupDescription.trim(), + avatar: this.data.groupAvatar, + memberIds: this.data.selectedMembers.map(member => member.userId), + settings: { + allowMemberInvite: this.data.allowMemberInvite, + saveToContacts: this.data.saveToContacts, + showQRCode: this.data.showQRCode + } + }; + + // 调用群聊管理器创建群聊 + const result = await groupChatManager.createGroup(groupInfo); + + this.setData({ + isCreating: false + }); + + if (result.success) { + wx.showToast({ + title: '群聊创建成功', + icon: 'success' + }); + + console.log('✅ 群聊创建成功:', result.data.groupId); + + // 跳转到群聊页面 + setTimeout(() => { + wx.redirectTo({ + url: `/pages/message/chat/chat?chatType=1&targetId=${result.data.groupId}&chatName=${encodeURIComponent(result.data.name)}` + }); + }, 1500); + + } else { + wx.showToast({ + title: result.error || '创建群聊失败', + icon: 'none' + }); + } + + } catch (error) { + this.setData({ + isCreating: false + }); + + console.error('❌ 创建群聊失败:', error); + wx.showToast({ + title: '创建群聊失败', + icon: 'none' + }); + } + }, + + // 🧭 ===== 页面导航 ===== + + // 返回上一页 + goBack() { + wx.navigateBack(); + } +}); diff --git a/pages/group/create-group/create-group.json b/pages/group/create-group/create-group.json new file mode 100644 index 0000000..5c144e6 --- /dev/null +++ b/pages/group/create-group/create-group.json @@ -0,0 +1,7 @@ +{ + "navigationStyle": "custom", + "backgroundColor": "#F2F2F7", + "backgroundTextStyle": "dark", + "enablePullDownRefresh": false, + "onReachBottomDistance": 50 +} diff --git a/pages/group/create-group/create-group.wxml b/pages/group/create-group/create-group.wxml new file mode 100644 index 0000000..966a663 --- /dev/null +++ b/pages/group/create-group/create-group.wxml @@ -0,0 +1,191 @@ + + + + + + + + + + + 创建群聊 + + + + 创建 + + + + + + + + + + + + + + 📷 + 设置群头像 + + + + + + + + 群名称 + * + + + + {{groupName.length}}/{{maxGroupNameLength}} + + + + + + + 群描述 + + + + + + + + + + + 关于我 + + + 职业 + + {{user.occupation || '请选择'}} + + + + + + 教育 + + {{user.education || '请选择'}} + + + + + + 性别 + + {{user.gender || '请选择'}} + + + + + + 生日 + + + {{user.birthday || '请选择'}} + + + + + + + 家乡 + + + 请选择 + {{user.hometown[0]}} {{user.hometown[1]}} {{user.hometown[2]}} + + + + + + + + 更多 + + + 星座 + {{user.constellation || '选择生日后会自动试算哦😘'}} + + + 身高 + {{user.height || '请选择'}} + + + 人格类型 + {{user.personality || '请选择'}} + + + 睡眠习惯 + {{user.sleep || '请选择'}} + + + 社交活跃度 + {{user.social || '请选择'}} + + + + + + + + + + + {{sheetTitle}} + + + + + {{item}} + + + + + + diff --git a/pages/personal-details/personal-details.wxss b/pages/personal-details/personal-details.wxss new file mode 100644 index 0000000..e266388 --- /dev/null +++ b/pages/personal-details/personal-details.wxss @@ -0,0 +1,247 @@ +/* 全局背景与字体 */ +page, .personal { + height: 100%; + background: #000000; + padding: 30rpx; + display: flex; + flex-direction: column; + position: relative; +} + +/* 容器 */ +.personal-container { + /* flex: 1; */ + background: transparent; + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} + +/* 卡片基础 */ +.card { + background: rgb(105 105 105 / 30%); + border-radius: 18rpx; + /* 减小整体内边距,给行内元素更多可用宽度 */ + padding: 12rpx 14rpx; + margin-bottom: 20rpx; + box-shadow: 0 6rpx 18rpx rgba(0,0,0,0.6); + color: #e8e8e8; +} + +/* 头像区域 */ +.avatar-area { + display: flex; + align-items: center; + justify-content: space-between; +} +.avatar-area .left .label { + font-size: 26rpx; + color: #cfcfcf; +} +/* .avatar-right { + display: flex; + align-items: center; +} */ +.avatar-img { + width: 110rpx; + height: 110rpx; + border-radius: 50%; + background: #000000; + margin-left: 18rpx; + margin-right: 18rpx; + display: block; +} + +/* 头像外层容器(如有使用) */ +.avatar-wrap { + width: 110rpx; + height: 110rpx; + border-radius: 50%; + overflow: hidden; + background: #000; + margin-left: 18rpx; + margin-right: 18rpx; + box-shadow: 0 6rpx 18rpx rgba(0,0,0,0.4); +} + +/* 头像占位样式(无头像时) */ +.avatar-placeholder { + width: 110rpx; + height: 110rpx; + border-radius: 50%; + background: linear-gradient(135deg, #151516 0%, #0F0F11 100%); + display: flex; + align-items: center; + justify-content: center; + color: #e8e8e8; + font-size: 40rpx; + margin-left: 18rpx; + margin-right: 18rpx; + box-shadow: 0 6rpx 18rpx rgba(0,0,0,0.4); +} +.chev { + color: #9b9b9b; + font-size: 30rpx; + margin-left: 8rpx; +} + +/* 小按钮样式 */ +.icon-btn { + /* 小按钮,移除强制宽度,允许在小屏上收缩 */ + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 28rpx; + width: 120rpx; + padding: 4rpx 8rpx; + font-size: 26rpx; + background-color: rgb(143 49 255 / 32%); + box-sizing: border-box; + flex: 0 0 auto; + line-height: 1; +} +/* 输入框样式 */ +.input { + font-size: 28rpx; + /* 可见的文本颜色 */ + color: #ffffff; + /* 左对齐输入,通常更自然且减少换行问题 */ + text-align: left; + flex: 1 1 auto; /* 主动占满剩余空间 */ + width: auto; + min-width: 0; /* 允许在 flex 容器中收缩,避免换行 */ + border: none; /* 移除默认边框 */ + background: transparent; + padding: 0 8rpx 0 0; +} + +/* 个人简介 */ +.intro-box { + background: rgb(105 105 105 / 30%); + border-radius: 12rpx; + padding: 16rpx; + min-height: 140rpx; +} +.intro-text { + color: #bfbfbf; + line-height: 1.6; + font-size: 24rpx; + white-space: normal; + word-wrap: break-word; + word-break: break-word; +} + +/* 编辑态 textarea */ +.textarea { + width: 95%; + min-height: 160rpx; + background: #0b0d0e; + color: #ddd; + border-radius: 12rpx; + padding: 16rpx; + font-size: 24rpx; + border: 1rpx solid rgba(255,255,255,0.02); +} +.intro-actions { + display: flex; + justify-content: flex-end; + margin-top: 12rpx; +} +.btn { + padding: 10rpx 20rpx; + margin-left: 12rpx; + border-radius: 12rpx; + font-size: 24rpx; + border: none; +} +.cancel { + background: transparent; + color: #9b9b9b; + border: 1rpx solid rgba(255,255,255,0.03); +} +.save { + background: linear-gradient(90deg,#00c2a8,#00a3ff); + color: #fff; +} + +/* 卡片样式 */ +.intro-card .row { + display: flex; + flex-wrap: nowrap; /* 禁止换行,保证左侧标签与右侧内容在同一行 */ + justify-content: space-between; + align-items: center; + /* 减小行内上下与左右内边距,释放水平空间 */ + padding: 22rpx 8rpx; + border-bottom: 1rpx solid rgba(255,255,255,0.02); +} +.intro-card .row:last-child { border-bottom: none; } +.left { color: #cfcfcf; font-size: 26rpx; /* 固定或最小宽度,避免被压缩换行 */ + /* 将左侧最小宽度适度减小,给输入留出更多空间 */ + min-width: 88rpx; + flex: 0 0 auto; + white-space: nowrap; /* 防止标签内换行 */ +} +.right { display:flex; align-items:center; flex: 1 1 auto; justify-content: flex-end; gap: 6rpx; } +.value { color: #e3e3e3; font-size: 26rpx; margin-right: 10rpx; } + +/* 底部选择面板(sheet)与遮罩 */ +.mask { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); + z-index: 50; +} +.sheet { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 60; + background: #0f1112; + border-top-left-radius: 28rpx; + border-top-right-radius: 28rpx; + padding: 18rpx; + box-shadow: 0 -8rpx 30rpx rgba(0,0,0,0.6); +} +.sheet-handle { + width: 80rpx; + height: 6rpx; + background: rgba(255,255,255,0.06); + border-radius: 6rpx; + margin: 6rpx auto 12rpx; +} +.sheet-title { + display:flex; + justify-content:space-between; + align-items:center; + padding-bottom: 12rpx; + border-bottom: 1rpx solid rgba(255,255,255,0.03); +} +.sheet-title text { color: white; font-size: 28rpx; } +.sheet-done { + background: transparent; + color: #9aa0a6; + border: none; + font-size: 24rpx; +} +.sheet-list { + max-height: 420rpx; + margin-top: 12rpx; + padding-bottom: 12rpx; +} +.sheet-item { + padding: 16rpx 12rpx; + display:flex; + justify-content:space-between; + align-items:center; + border-bottom: 1rpx solid rgba(255,255,255,0.02); + font-size: 26rpx; + color: #d7d7d7; +} +.sheet-item.active { + background: rgba(0,160,255,0.06); + color: #00a3ff; +} +.sheet-check { color: #00a3ff; font-size: 28rpx; } diff --git a/pages/profile/profile.js b/pages/profile/profile.js new file mode 100644 index 0000000..ea6f05b --- /dev/null +++ b/pages/profile/profile.js @@ -0,0 +1,805 @@ +// Personal Profile Page Logic +const app = getApp(); +const config = require('../../config/config.js'); +const apiClient = require('../../utils/api-client.js'); +const authManager = require('../../utils/auth.js'); +const imageCacheManager = require('../../utils/image-cache-manager.js'); + +Page({ + data: { + // User Information + userInfo: null, + + // Statistics + stats: { + friendsCount: 0, + postsCount: 0, + visitorsCount: 0, + likesCount: 0, + groupsCount: 0 + }, + + // App Information + newMomentsCount: 0, + fileSize: '0MB', + cacheSize: '0MB', + appVersion: '', + + // UI State + showQRModal: false, + selectedTab: 'gender', + + // System Adaptation Information + systemInfo: {}, + statusBarHeight: 0, + menuButtonHeight: 0, + menuButtonTop: 0, + navBarHeight: 0, + windowHeight: 0, + safeAreaBottom: 0, + + // Debug Information + debugInfo: { + hasGlobalUserInfo: false, + hasToken: false, + tokenLength: 0, + hasLocalStorage: false + }, + + // Authentication State + isLoggedIn: false, + + // Settings State + currentTheme: 'Auto', + notificationStatus: 'Enabled', + currentLanguage: 'Chinese' + }, + + /** + * Page Lifecycle Methods + */ + onLoad: function (options) { + console.log('Personal Center Page Loaded'); + this.initSystemInfo(); + this.initData(); + }, + + onShow: function () { + console.log('Personal Center Page Shown'); + this.loadUserData(); + this.loadUserStats(); + }, + + onReady: function () { + console.log('Personal Center Page Ready'); + }, + + onHide: function () { + console.log('Personal Center Page Hidden'); + }, + + onUnload: function () { + console.log('Personal Center Page Unloaded'); + }, + + /** + * Initialization Methods + */ + initData() { + try { + const userInfo = app.globalData.userInfo; + this.setData({ + userInfo: userInfo || { + user: { + nickname: 'Nickname Not Set', + customId: '123456789', + avatar: '', + signature: '', + gender: 'male', + verified: false + }, + age: null, + mood: '', + personality: '', + identity: '', + constellation: '', + school: '', + occupation: '' + }, + appVersion: config.appVersion || '1.0.0', + isLoggedIn: app.globalData.isLoggedIn || false + }); + } catch (error) { + console.error('Failed to initialize data:', error); + } + }, + + initSystemInfo() { + try { + const systemInfo = wx.getSystemInfoSync(); + const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); + + const statusBarHeight = systemInfo.statusBarHeight || 0; + const menuButtonHeight = menuButtonInfo.height || 0; + const menuButtonTop = menuButtonInfo.top || 0; + const menuButtonBottom = menuButtonInfo.bottom || 0; + const navBarHeight = menuButtonBottom + menuButtonTop - statusBarHeight; + const windowHeight = systemInfo.windowHeight || 0; + const safeAreaBottom = systemInfo.safeArea ? + systemInfo.screenHeight - systemInfo.safeArea.bottom : 0; + + this.setData({ + systemInfo, + statusBarHeight, + menuButtonHeight, + menuButtonTop, + navBarHeight, + windowHeight, + safeAreaBottom + }); + + console.log('System adaptation info:', { + statusBarHeight, + menuButtonHeight, + menuButtonTop, + navBarHeight, + windowHeight, + safeAreaBottom + }); + } catch (error) { + console.error('Failed to initialize system info:', error); + } + }, + + /** + * Data Loading Methods + */ + loadUserData() { + try { + const globalUserInfo = app.globalData.userInfo; + const isLoggedIn = app.globalData.isLoggedIn; + const currentToken = apiClient.getToken(); + + // Ensure user information contains customId + let userInfo = globalUserInfo; + if (userInfo?.user) { + const user = userInfo.user; + if (!user.customId && user.id) { + user.customId = 'findme_' + user.id; + } + if (!user.customId) { + user.customId = 'Not Set'; + } + } + + // Debug information + const debugInfo = { + hasGlobalUserInfo: !!globalUserInfo, + hasToken: !!currentToken, + tokenLength: currentToken ? currentToken.length : 0, + tokenPrefix: currentToken ? currentToken.substring(0, 20) + '...' : 'null', + hasLocalStorage: false + }; + + // Check local storage + try { + const storedUserInfo = wx.getStorageSync('userInfo'); + debugInfo.hasLocalStorage = !!(storedUserInfo?.token); + } catch (storageError) { + console.warn('Failed to check local storage:', storageError); + } + + console.log('Personal Center Debug Information:', debugInfo); + + this.setData({ + userInfo, + isLoggedIn, + debugInfo + }); + } catch (error) { + console.error('Failed to load user data:', error); + } + }, + + async loadUserStats() { + try { + const response = await this.mockLoadStats(); + + if (response?.code === 0) { + this.setData({ + stats: response.data.stats || this.data.stats, + newMomentsCount: response.data.newMomentsCount || 0, + fileSize: response.data.fileSize || '0MB', + cacheSize: response.data.cacheSize || '0MB' + }); + } + } catch (error) { + console.error('Failed to load user stats:', error); + } + }, + + async mockLoadStats() { + // Simulate network delay + await new Promise(resolve => setTimeout(resolve, 300)); + + return { + code: 0, + message: 'Success', + data: { + stats: { + friendsCount: 42, + postsCount: 18, + visitorsCount: 156, + likesCount: 89, + groupsCount: 5 + }, + newMomentsCount: 2, + fileSize: '125MB', + cacheSize: '32MB' + } + }; + }, + + refreshUserInfo() { + const userInfo = app.globalData.userInfo; + if (userInfo) { + this.setData({ userInfo }); + } + }, + + /** + * Avatar Management + */ + changeAvatar() { + wx.showActionSheet({ + itemList: ['Take Photo', 'Choose from Album'], + success: (res) => { + const sourceType = res.tapIndex === 0 ? ['camera'] : ['album']; + + wx.chooseMedia({ + count: 1, + mediaType: ['image'], + sourceType: sourceType, + maxDuration: 30, + camera: 'back', + success: (res) => { + console.log('Avatar selection successful:', res.tempFiles[0]); + this.uploadAvatar(res.tempFiles[0].tempFilePath); + }, + fail: (error) => { + console.error('Failed to select avatar:', error); + wx.showToast({ + title: 'Failed to Select Avatar', + icon: 'none' + }); + } + }); + } + }); + }, + + async uploadAvatar(tempFilePath) { + try { + wx.showLoading({ title: 'Uploading...' }); + + const uploadResult = await this.uploadAvatarToServer(tempFilePath); + + if (uploadResult.success) { + // Cache new avatar + const cachedAvatarUrl = await imageCacheManager.updateAvatarCache( + this.data.userInfo?.user?.avatar, + uploadResult.avatarUrl + ); + + // Update local user information + const userInfo = { ...this.data.userInfo }; + if (userInfo?.user) { + userInfo.user.avatar = cachedAvatarUrl; + this.setData({ userInfo }); + + // Update global user information + if (app.globalData.userInfo?.user) { + app.globalData.userInfo.user.avatar = cachedAvatarUrl; + } + } + + wx.hideLoading(); + wx.showToast({ + title: 'Avatar Updated Successfully', + icon: 'success' + }); + } else { + throw new Error(uploadResult.message || 'Upload Failed'); + } + } catch (error) { + wx.hideLoading(); + console.error('Failed to upload avatar:', error); + wx.showToast({ + title: error.message || 'Upload Failed', + icon: 'none' + }); + } + }, + + async uploadAvatarToServer(tempFilePath) { + try { + console.log('Starting avatar upload:', tempFilePath); + + const uploadResult = await new Promise((resolve, reject) => { + wx.uploadFile({ + url: `${config.api.baseUrl}/api/v1/file/upload`, + filePath: tempFilePath, + name: 'file', + formData: { + file_type: 'avatar', + usage_type: 'avatar' + }, + header: { + 'Authorization': `Bearer ${apiClient.getToken()}` + }, + success: resolve, + fail: reject + }); + }); + + console.log('Upload response:', uploadResult); + + if (uploadResult.statusCode === 200) { + const result = JSON.parse(uploadResult.data); + if (result.code === 0) { + return { + success: true, + avatarUrl: result.data.file_url, + message: result.message || 'Upload Successful' + }; + } else { + return { + success: false, + message: result.message || 'Upload Failed' + }; + } + } else { + return { + success: false, + message: `HTTP Error: ${uploadResult.statusCode}` + }; + } + } catch (error) { + console.error('Failed to upload avatar to server:', error); + return { + success: false, + message: error.message || 'Network Error' + }; + } + }, + + /** + * Tab Selection + */ + onTabSelect(e) { + const tab = e.currentTarget.dataset.tab; + if (tab) { + this.setData({ selectedTab: tab }); + } + }, + + /** + * Navigation Methods + */ + editProfile() { + wx.navigateTo({ + url: '/pages/personal-details/personal-details' + }); + }, + + openSettingsPage() { + wx.navigateTo({ + url: '/pages/settingss/settingss' + }); + }, + + navigateToQRCode() { + wx.navigateTo({ + url: '/pages/qr-code/qr-code' + }); + }, + + viewFriends() { + wx.navigateTo({ + url: '/pages/social/friends/friends' + }); + }, + + /** + * Statistics Navigation + */ + viewPosts() { + wx.showToast({ + title: 'Moments Function Has Been Removed', + icon: 'none' + }); + }, + + viewVisitors() { + wx.showToast({ + title: 'Visitor Records Available in the APP', + icon: 'none' + }); + }, + + viewLikes() { + wx.showToast({ + title: 'Like Records Available in the APP', + icon: 'none' + }); + }, + + /** + * Settings Methods + */ + openThemeSettings() { + const themes = ['Light', 'Dark', 'Auto']; + wx.showActionSheet({ + itemList: themes, + success: (res) => { + const selectedTheme = themes[res.tapIndex]; + this.setData({ currentTheme: selectedTheme }); + wx.showToast({ + title: `Switched to ${selectedTheme} Theme`, + icon: 'success' + }); + + // Save theme preference + wx.setStorageSync('theme', selectedTheme); + } + }); + }, + + openNotificationSettings() { + const options = ['Enabled', 'Disabled', 'Do Not Disturb']; + wx.showActionSheet({ + itemList: options, + success: (res) => { + const selectedStatus = options[res.tapIndex]; + this.setData({ notificationStatus: selectedStatus }); + + const statusText = selectedStatus === 'Enabled' ? 'Enabled' : + selectedStatus === 'Disabled' ? 'Disabled' : + 'Set to Do Not Disturb'; + + wx.showToast({ + title: `Notifications ${statusText}`, + icon: 'success' + }); + + // Save notification preference + wx.setStorageSync('notificationStatus', selectedStatus); + } + }); + }, + + openLanguageSettings() { + const languages = ['Chinese', 'English']; + wx.showActionSheet({ + itemList: languages, + success: (res) => { + const selectedLanguage = languages[res.tapIndex]; + this.setData({ currentLanguage: selectedLanguage }); + wx.showToast({ + title: `Switched to ${selectedLanguage}`, + icon: 'success' + }); + + // Save language preference + wx.setStorageSync('language', selectedLanguage); + } + }); + }, + + openChatSettings() { + wx.showActionSheet({ + itemList: ['Font Size', 'Chat Background', 'Message Preview'], + success: (res) => { + const options = ['Font Size', 'Chat Background', 'Message Preview']; + const selectedOption = options[res.tapIndex]; + wx.showToast({ + title: `${selectedOption} Function Under Development`, + icon: 'none' + }); + } + }); + }, + + /** + * Feature Methods (Currently Limited) + */ + openWallet() { + wx.showToast({ + title: 'Please Experience in the APP', + icon: 'none' + }); + }, + + openCards() { + wx.showToast({ + title: 'Please Experience in the APP', + icon: 'none' + }); + }, + + openStickers() { + wx.showToast({ + title: 'Please Experience in the APP', + icon: 'none' + }); + }, + + openGames() { + wx.showToast({ + title: 'Please Experience in the APP', + icon: 'none' + }); + }, + + openShopping() { + wx.showToast({ + title: 'Function Under Development', + icon: 'none' + }); + }, + + openBackupSettings() { + wx.showToast({ + title: 'Please Experience in the APP', + icon: 'none' + }); + }, + + viewProfile() { + wx.showToast({ + title: 'Personal Homepage Available in the APP', + icon: 'none' + }); + }, + + managePrivacy() { + wx.showToast({ + title: 'Privacy Settings Available in the APP', + icon: 'none' + }); + }, + + viewMoments() { + wx.showToast({ + title: 'Moments Function Has Been Removed', + icon: 'none' + }); + }, + + viewFavorites() { + wx.showToast({ + title: 'My Favorites Available in the APP', + icon: 'none' + }); + }, + + viewGroups() { + wx.showToast({ + title: 'My Groups Available in the APP', + icon: 'none' + }); + }, + + viewFiles() { + wx.showToast({ + title: 'File Management Available in the APP', + icon: 'none' + }); + }, + + /** + * Cache Management + */ + clearCache() { + wx.showModal({ + title: 'Clear Cache', + content: 'Are you sure you want to clear the cache? Some data may need to be reloaded after clearing.', + success: (res) => { + if (res.confirm) { + wx.showLoading({ title: 'Clearing...' }); + + // Clear image cache + imageCacheManager.clearAllCache(); + + setTimeout(() => { + wx.hideLoading(); + this.setData({ cacheSize: '0MB' }); + wx.showToast({ + title: 'Cache Cleared Successfully', + icon: 'success' + }); + }, 1000); + } + } + }); + }, + + showCacheStats() { + try { + const stats = imageCacheManager.getCacheStats(); + wx.showModal({ + title: 'Cache Statistics', + content: `Total Cache: ${stats.total}\nAvatar Cache: ${stats.avatar}\nImage Cache: ${stats.image}\nExpired Cache: ${stats.expired}\nMax Cache: ${stats.maxSize}`, + showCancel: false, + confirmText: 'OK' + }); + } catch (error) { + console.error('Failed to show cache stats:', error); + wx.showToast({ + title: 'Failed to Load Cache Stats', + icon: 'none' + }); + } + }, + + /** + * App Information Methods + */ + checkUpdates() { + wx.showLoading({ title: 'Checking...' }); + + setTimeout(() => { + wx.hideLoading(); + wx.showToast({ + title: 'You Are Using the Latest Version', + icon: 'success' + }); + }, 1000); + }, + + aboutApp() { + wx.showModal({ + title: 'About FindMe', + content: `FindMe v${this.data.appVersion}\n\nA location-based social application\nDiscover nearby, connect the world\n\n© 2025 FindMe`, + showCancel: false, + confirmText: 'Got it' + }); + }, + + viewHelp() { + wx.showToast({ + title: 'Help Center Available in the APP', + icon: 'none' + }); + }, + + giveFeedback() { + wx.showModal({ + title: 'Feedback', + editable: true, + placeholderText: 'Please enter your comments or suggestions...', + success: (res) => { + if (res.confirm && res.content?.trim()) { + wx.showLoading({ title: 'Submitting...' }); + + // Here you would typically call an API to submit feedback + setTimeout(() => { + wx.hideLoading(); + wx.showToast({ + title: 'Feedback Submitted Successfully', + icon: 'success' + }); + }, 1000); + } + } + }); + }, + + /** + * QR Code Methods + */ + showQRCode() { + this.setData({ showQRModal: true }); + }, + + hideQRCode() { + this.setData({ showQRModal: false }); + }, + + saveQRCode() { + wx.showToast({ + title: 'Please Experience in the APP', + icon: 'none' + }); + }, + + shareQRCode() { + wx.showToast({ + title: 'Please Experience in the APP', + icon: 'none' + }); + }, + + /** + * Debug Methods + */ + async testApiCall() { + try { + wx.showLoading({ title: 'Testing API...' }); + + const response = await apiClient.getUserInfo(); + + wx.hideLoading(); + wx.showModal({ + title: 'API Test Successful', + content: `User Information Retrieved: ${JSON.stringify(response.data)}`, + showCancel: false + }); + } catch (error) { + wx.hideLoading(); + wx.showModal({ + title: 'API Test Failed', + content: `Error Message: ${error.message}`, + showCancel: false + }); + } + }, + + refreshDebugInfo() { + this.loadUserData(); + wx.showToast({ + title: 'Refreshed', + icon: 'success' + }); + }, + + /** + * Authentication Methods + */ + async logout() { + wx.showModal({ + title: 'Logout', + content: 'Are you sure you want to log out of the current account?', + success: (res) => { + if (res.confirm) { + this.performLogout(); + } + } + }); + }, + + async performLogout() { + try { + wx.showLoading({ title: 'Logging Out...' }); + + const success = await app.logout(); + + wx.hideLoading(); + + if (success) { + wx.showToast({ + title: 'Logged Out Successfully', + icon: 'success' + }); + + setTimeout(() => { + wx.reLaunch({ + url: '/pages/login/login' + }); + }, 1500); + } else { + wx.showToast({ + title: 'Logout Failed', + icon: 'none' + }); + } + } catch (error) { + wx.hideLoading(); + console.error('Logout failed:', error); + wx.showToast({ + title: 'Logout Error', + icon: 'none' + }); + } + } +}); \ No newline at end of file diff --git a/pages/profile/profile.json b/pages/profile/profile.json new file mode 100644 index 0000000..963151e --- /dev/null +++ b/pages/profile/profile.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "个人资料", + "navigationBarTextStyle": "white", + "disableScroll": true +} \ No newline at end of file diff --git a/pages/profile/profile.wxml b/pages/profile/profile.wxml new file mode 100644 index 0000000..95a32c4 --- /dev/null +++ b/pages/profile/profile.wxml @@ -0,0 +1,305 @@ + + + + + + + + + + + + + + 📷 + + 👑 + + + + + + + {{userInfo.user.nickname || 'FindMe用户'}} + + {{userInfo.user.gender === 'male' ? '♂️' : userInfo.user.gender === 'female' ? '♀️' : ''}} + + 👑 + + + + ID: + {{userInfo.user.customId || (userInfo.user.id ? 'findme_' + userInfo.user.id : '未设置')}} + + + + 去认证 + + + + 已认证 + + + + + + + + + + + + + + + + + 编辑 + + + + + + + + + + + 291, Anwar Yousuf Road + + + + + + + Morem ipsum dolor sit amet, consectetur adipiscing elit. Nunc vulputate libero et velit interdum, ac aliquet odio mattis. + + + + + + + {{userInfo.user.gender === 'male' ? '♂️' : userInfo.user.gender === 'female' ? '♀️' : '?'}} + + + + 年龄 {{userInfo.age}} + + + + 心情 {{userInfo.mood}} + + + + 人格 {{userInfo.personality}} + + + + 身份 {{userInfo.identity}} + + + + 星座 {{userInfo.constellation}} + + + + 学校 {{userInfo.school}} + + + + 职业 {{userInfo.occupation}} + + + + + + + + + + + + + 1个NOW,2个地点 + + + + + +我的动态 + + + + + + + + + + + + + + 26 Sep + 2025 + + + + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Ea eveniet, delectus unde quibusdam ipsum fugiat nostrum rerum maiores quisquam enim? + + + + + + + + + + + + + + + + + + + 🔧 + 调试信息 + + {{showDebugDetails ? '隐藏' : '展开'}} + + + + + + 用户信息: + {{debugInfo.userInfo}} + + + + Token状态: + + {{debugInfo.tokenValid ? '✅ 有效' : '❌ 无效'}} + + + + + 网络状态: + {{debugInfo.networkType || '未知'}} + + + + 版本信息: + {{debugInfo.version || 'v1.0.0'}} + + + + + 测试API + + + 缓存统计 + + + 清除缓存 + + + 导出日志 + + + + + + + + + + + + + + + 设置状态 + + + + + + + {{item.icon}} + {{item.text}} + + + + + + + + + + 取消 + 保存 + + + + + + + + + \ No newline at end of file diff --git a/pages/profile/profile.wxss b/pages/profile/profile.wxss new file mode 100644 index 0000000..a6d62a9 --- /dev/null +++ b/pages/profile/profile.wxss @@ -0,0 +1,861 @@ +/* 个人资料页面 - 简化兼容版 */ +@import "../../styles/design-system.wxss"; +@import "../../styles/components.wxss"; + +/* 页面主容器样式 */ +.profile-container { + height: 874px; + background: #000000; + display: flex; + flex-direction: column; + position: relative; +} + +/* 滚动内容区样式 */ +.profile-content { + flex: 1; + background: transparent; + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} + +/* 个人信息卡片样式 */ +.profile-card { + backdrop-filter: none; + border-radius: 24px; + margin: 16px; + position: relative; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.profile-card::before { + display: none; +} + +/* 顶部区域样式 */ +.profile-top { + display: flex; + align-items: center; +} + +.avatar-section { + margin: 16px 16px 16px 16px; +} + +.avatar-container { + background: rgba(255, 255, 255, 0.15); + width: 100px; + height: 100px; + position: relative; + border-radius: 999px; + overflow: hidden; + box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3); + transition: all 0.3s ease; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.avatar-container:active { + transform: scale(0.95); + box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4); +} + +.avatar-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* 在线状态指示器 */ +.online-status.online { + position: absolute; + top: 4px; + right: 4px; + width: 14px; + height: 14px; + background: #4CAF50; + border: 2px solid #ffffff; + border-radius: 50%; + box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3); + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7); } + 70% { box-shadow: 0 0 0 6px rgba(76, 175, 80, 0); } + 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); } +} + +.profile-main-info { + flex: 1; +} + +.profile-name-row { + display: flex; + align-items: center; + margin-bottom: 10px; + flex-wrap: nowrap; + overflow: hidden; +} + +.profile-name { + font-size: 24px; + font-weight: 500; + color: #ffffff; + line-height: 1.2; + margin-right: 10px; +} + +.profile-id { + display: flex; + align-items: center; + padding: 6px 0px; + width: fit-content; +} + +.id-label { + font-size: 14px; + color: #f3f3f3; + font-weight: 400; + margin-right: 6px; +} + +.id-value { + font-size: 14px; + color: #ffffff; + font-weight: 400; + font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; + margin-right: 6px; +} + +.verify-btn { + color: white; + font-size: 14px; + font-weight: 400; + padding: 3px 10px; + border-radius: 12px; + margin-left: 8px; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + line-height: 1; +} + +.verified-tag { + color: #fa6294; + font-size: 14px; + font-weight: 400; + padding: 3px 10px; + border-radius: 12px; + margin-left: 8px; + border: 1px solid #50a853; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + line-height: 1; +} + +.verify-btn-p { + width: 22px; + height: 22px; + padding: 2px; + border-radius: 4px; + display: block; + object-fit: contain; +} + +.verified-tag-p { + width: 22px; + height: 22px; + padding: 2px; + border-radius: 4px; + display: block; + object-fit: contain; +} + +/* 底部信息区样式 */ +.profile-bottom { + background: linear-gradient(123deg, #8361FB 15.54%, #70AAFC 39.58%, #F0F8FB 62.43%, #F07BFF 90.28%); + border-radius: 24px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.action-buttons { + display: flex; + width: fit-content; + margin-left: auto; + justify-content: flex-end; + gap: 2px; + margin-top: 1rem; +} + +.qr-code-btn { + padding: 4px 8px; + display: flex; + align-items: center; + transition: all 0.2s ease; +} + +.edit-btn { + background: linear-gradient(124deg, #FF6460 1.58%, #EC42C8 34.28%, #435CFF 54%, #00D5FF 84.05%); + border-radius: 999px; + padding: 0px 1rem; + display: flex; + align-items: center; + transition: all 0.2s ease; +} + +.setting-btn { + padding: 4px 6px; + display: flex; + align-items: center; + transition: all 0.2s ease; + margin-left: .5rem; +} + +.qr-code-icon { + width: 22px; + height: 22px; + margin-right: 6px; +} + +.edit-icon { + width: 16px; + height: 16px; + margin-right: 6px; +} + +.setting-icon { + width: 20px; + height: 20px; + margin-right: 6px; +} + +.edit-text { + font-size: 14px; + font-weight: 400; + color: black; +} + +.profile-location { + background: rgba(43, 43, 43, 1); + width:fit-content; + display: flex; + align-items: center; + padding: 8px 12px; + border-radius: 24px; + margin-left: 16px; +} + +.location-icon { + width: 20px; + height: 20px; + margin-right: 6px; +} + +.location-text { + font-size: 15px; + color: #e0e0e0; + line-height: 1.4; +} + +.profile-signature { + height: 120px; + display: flex; + align-items: flex-start; + padding: 12px; + background: rgba(90, 90, 90, 0.548); + border-radius: 12px; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), inset 0 0 10px rgba(255, 255, 255, 0.1); + margin-right:16px; + margin-left: 16px; +} + +.signature-text { + font-size: 15px; + color: #000000; + margin: auto; + font-weight: 400; +} + +.profile-tags { + display: flex; + flex-wrap: wrap; + gap: 10px; + padding: 12px; + border-radius: 12px; + margin-right:16px; + margin-left: 16px; +} + +.tag-item { + display: flex; + align-items: center; + background: rgba(90, 90, 90, 0.3); + border-radius: 12px; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), inset 0 0 10px rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 5px 12px; + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.tag-label { + font-size: 13px; + color: #ffffff; + margin-right: 6px; + font-weight: 400; +} + +.tag-value { + font-size: 13px; + color: #ffffff; +} + +.qr-code-btn:active, .edit-btn:active, .setting-btn:active { + transform: scale(0.95); + opacity: 0.8; +} + +/* 会员卡片样式 */ +.membership-card { + width: calc(100% - 32px); + height: 220px; + margin: 0 16px 16px; + border-radius: 12px; + padding: 24px; + display: flex; + justify-content: space-between; + background: linear-gradient(152deg, rgba(19, 157, 255, 0.3), rgba(49, 55, 234, 0.3), rgba(59, 196, 147, 0.3)); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05); + border: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08), inset 0 0 1px rgba(255, 255, 255, 0.5); + position: relative; + overflow: hidden; +} + +.membership-left { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + width: 55%; +} + +.logo-header { + display: flex; + align-items: center; + margin-bottom: 20px; +} + +.logo-image { + width: 48px; + height: 39px; + margin-right: 12px; +} + +.logo-text { + color: #FFF; + font-family: 'Inter', sans-serif; + font-size: 20px; + font-weight: 400; + white-space: nowrap; +} + +.benefit-tag { + display: flex; + align-items: center; + width: fit-content; + height:fit-content; + margin-left: 20px; + margin-bottom: 25px; +} + +.benefit-content { + background: #1C4EFE; + border-radius: 9999px; + padding: 8px 16px; + width: fit-content; + display: flex; + align-items: center; + justify-content: center; +} + +.benefit-text { + color: #FFF; + font-family: 'Inter', sans-serif; + font-size: 14px; + font-weight: 800; + white-space: nowrap; +} + +.check-icon { + width: 16px; + height: 16px; + margin-left: 2px; + flex-shrink: 0; + object-fit: contain; + align-self: center !important; +} + +.get-button { + display: flex; + justify-content: center; + align-items: center; + width: 150px; + height: 50px; + border-radius: 999px; + border: 1px solid #4E4E4E; + background: linear-gradient(262deg, #000 11.88%, #232323 91.52%); + box-shadow: 8px 0 12px 0 rgba(46, 173, 251, 0.25) inset; + padding: 0; + margin: 0; +} + +.get-button-text { + color: #FFF; + font-family: 'Poppins', sans-serif; + font-size: 25px; + font-weight: 600; + white-space: nowrap; + line-height: 1; +} + +.cards-container { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 45%; +} + +.card-stack { + position: relative; + width: 200px; + height: 168px; + display: flex; + justify-content: center; + align-items: center; +} + +.vip-card { + position: absolute; + width: 170px; + height: 119px; + border-radius: 14px; + box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25), 0 1px 1px 0 rgba(165, 165, 165, 0.25) inset, 0 0 1px 0 rgba(217, 217, 217, 0.20) inset; + transform-origin: center; +} + +.card-15deg { + transform: rotate(-15deg)translate(-6px, 6px); + z-index: 3; +} + +.card-10deg { + transform: rotate(-10deg); + z-index: 2; +} + +.card-5deg { + transform: rotate(-5deg)translate(6px, -6px); + z-index: 1; +} + +/* 功能模块样式 */ +.quick-actions { + background: linear-gradient(123deg, rgba(19, 157, 255, 0.4) 14.61%, rgba(49, 55, 234, 0.4) 53.02%, rgba(59, 196, 147, 0.4) 96.97%); + backdrop-filter: blur(20px) saturate(180%); + -webkit-backdrop-filter: blur(20px) saturate(180%); + border-radius: 20px; + margin: 0 16px 16px; + padding: 20px; + border: 1px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1), inset 0 0 16px rgba(255, 255, 255, 0.1); + position: relative; + overflow: hidden; +} + +.quick-actions::before { + content: ""; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle at 70% 30%, rgba(255, 255, 255, 0.15) 0%, transparent 60%); + transform: rotate(30deg); + pointer-events: none; +} + +.action-row { + display: flex; + justify-content: space-around; +} + +.action-item { + display: flex; + flex-direction: column; + align-items: center; + padding: 12px; + transition: all 0.3s ease; + position: relative; + transition: all 0.3s cubic-bezier(0.25, 0.1, 0.25, 1.5); +} + +.action-item:active { + transform: scale(0.96) translateY(1px); + background: rgba(102, 126, 234, 0.1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) inset, 0 1px 2px rgba(255, 255, 255, 0.2); + filter: brightness(0.98); +} + +.action-icon { + width: 48px; + height: 48px; + margin-bottom: 8px; + display: block; + object-fit: contain; + background-color: transparent; + pointer-events: none; +} + +.action-text { + font-size: 20px; + color: rgb(255, 255, 255); + font-weight: 400; + text-align: center; +} + +.menu-section { + margin: 0 16px 16px; + padding-bottom: 20px; +} + +.menu-group { + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(20px); + border-radius: 16px; + margin-bottom: 12px; + overflow: hidden; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.group-header { + display: flex; + align-items: center; + padding: 16px 20px 8px; + background: rgba(102, 126, 234, 0.05); +} + +.group-icon { + font-size: 16px; + color: #667eea; + margin-right: 8px; +} + +.group-title { + font-size: 14px; + font-weight: 600; + color: #2c3e50; +} + +.menu-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + transition: all 0.2s ease; + position: relative; + overflow: hidden; +} + +.menu-item:last-child { + border-bottom: none; +} + +.menu-item:active { + background: rgba(102, 126, 234, 0.05); +} + +.menu-left { + display: flex; + align-items: center; + flex: 1; +} + +.menu-icon { + width: 32px; + height: 32px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + color: #ffffff; + margin-right: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.menu-content { + flex: 1; +} + +.menu-title { + font-size: 16px; + font-weight: 500; + color: #2c3e50; + line-height: 1.2; + margin-bottom: 2px; +} + +.menu-subtitle { + font-size: 12px; + color: #666; + line-height: 1.3; +} + +.menu-right { + display: flex; + align-items: center; +} + +.menu-badge { + background: #ff4757; + color: white; + font-size: 10px; + font-weight: 600; + padding: 2px 6px; + border-radius: 8px; + margin-right: 8px; + min-width: 16px; + text-align: center; +} + +.menu-arrow { + color: #bbb; + font-size: 18px; + font-weight: 300; +} + +.logout-section { + margin: 0 16px 16px; +} + +.logout-btn { + background: rgba(255, 71, 87, 0.1); + border: 1px solid rgba(255, 71, 87, 0.2); + border-radius: 16px; + padding: 16px 20px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + backdrop-filter: blur(10px); +} + +.logout-btn:active { + background: rgba(255, 71, 87, 0.15); + transform: scale(0.98); +} + +.logout-icon { + font-size: 16px; + color: #ff4757; + margin-right: 8px; +} + +.logout-text { + font-size: 16px; + color: #ff4757; + font-weight: 600; +} + +.bottom-space { + margin-bottom: env(safe-area-inset-bottom); + min-height: 40px; +} + +/* 响应式设计 */ +@media (max-width: 375px) { + .profile-header { + flex-direction: column; + align-items: center; + text-align: center; + } + + .avatar-section { + margin-right: 0; + margin-bottom: 16px; + } + + .profile-stats { + justify-content: center; + } + + .action-item { + padding: 8px; + } + + .action-icon { + width: 40px; + height: 40px; + font-size: 16px; + } + + .action-text { + font-size: 11px; + } +} + + +.profile-tabs { + width: 100%; + margin-bottom: 1rem; +} + +.tab-scroll { + display: flex; + flex-direction: row; + white-space: nowrap; +} + +.tab-item { + margin-top: .5rem; + padding: 15rpx 28rpx; /* smaller padding vertically + horizontally */ + font-size: 25rpx; /* slightly smaller text */ + line-height: 45rpx; /* controls the text vertical alignment */ + height: 60rpx; /* explicit height for the box */ + display: flex; /* ensures vertical centering */ + align-items: center; /* centers text in the box */ + color: white; + border-radius: 10px; + background: rgba(90, 90, 90, 0.548); + transition: all 0.2s; + margin-left: .3rem; + margin-right: .3rem; +} + + + +.tab-item.active { + color: #fff; + background: rgba(90, 90, 90, 0.836); + font-weight: bold; +} +.test-text{ + color: white; +} +.profile-bottom { + background: linear-gradient(123deg, #8361FB 15.54%, #70AAFC 39.58%, #F0F8FB 62.43%, #F07BFF 90.28%); + border-radius: 24px; + display: flex; + flex-direction: column; + gap: 16px; +} +.input-box { + display: flex; + align-items: center; + border: 1px solid lightgray; + border-radius: 25rpx; + padding: .8rem; /* inner padding */ + margin: 20rpx 35rpx; /* outer margin */ + color: white; +} + +.input-field { + flex: 1; /* take full width */ + font-size: 30rpx; + border: none; /* remove native border */ + outline: none; /* remove focus outline */ +} + +.camera-icon { + width: 50rpx; + height: 50rpx; + margin-left: 12rpx; + color: white; +} +.moments { + padding: 30rpx; +} + +.moment-card { + border: 1px solid #eee; + border-radius: 12rpx; + padding: 20rpx; + color: white; +} + +.moment-header { + display: flex; + justify-content: space-between; /* space between date and year */ + font-size: 28rpx; + font-weight: bold; +} + +.moment-text { + font-size: 26rpx; + color: white; + line-height: 1.5; + margin-bottom: 15rpx; + display: block; +} + +.moment-img { + width: 100%; + border-radius: 8rpx; +} +.textcolor{ + color: white; + margin-left: 1.5rem; +} +.myfootprint{ + margin: 30rpx; + padding: 20rpx; + border-radius: 20rpx; + background: linear-gradient(123deg, #8361FB 15.54%, #70AAFC 39.58%, #F0F8FB 62.43%, #F07BFF 90.28%); + position: relative; + height: 200rpx; + display: flex; + align-items: center; + justify-content: center; +} + +/* White pill badge */ +.footprint-badge { + position: absolute; + bottom: 30rpx; + right: 30rpx; + background: #fff; + padding: 8rpx 20rpx; + border-radius: 30rpx; + z-index: 3; +} + +.footprint-badge text { + font-size: 26rpx; + font-weight: bold; + color: #000; +} + + + + + diff --git a/pages/qr-code/qr-code.js b/pages/qr-code/qr-code.js new file mode 100644 index 0000000..1fc046e --- /dev/null +++ b/pages/qr-code/qr-code.js @@ -0,0 +1,484 @@ +// Get application instance +const app = getApp(); +const apiClient = require('../../utils/api-client.js'); // Import your API client + +// Page configuration +Page({ + // Page data + data: { + username: 'Loading...', // User name (loading state) + userId: 'Loading...', // User ID (loading state) + qrCodeUrl: '', // QR code image URL + isDarkMode: false, // Whether in dark mode + isLoading: true, // Loading state + userInfo: null, // Store complete user info + isTestData: false, // Flag to indicate if using test data + }, + + // Test/fallback user data to prevent crashes + getTestUserData: function() { + return { + user: { + id: 'test_123456', + customId: 'TEST001', + nickname: 'Test User', + phone: '13800138000', + avatar: 'https://via.placeholder.com/100x100/4CAF50/white?text=T', + gender: 1, + birthday: '1990-01-01', + location: 'Test City', + signature: 'This is a test user for development', + isActive: true, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z' + }, + token: 'test_token_123456789', + refreshToken: 'test_refresh_token_123456789', + expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000), // 7 days from now + permissions: ['basic', 'location', 'social'], + settings: { + locationPrivacy: 'friends', + showPhone: false, + allowSearch: true + } + }; + }, + + // Page load lifecycle function + onLoad: function() { + console.log('QR Code page loaded'); + + // Check theme settings first + const isDarkMode = wx.getStorageSync('isDarkMode') || false; + this.setData({ + isDarkMode: isDarkMode + }); + + // Load user information + this.loadUserInfo(); + }, + + // Load user information with guaranteed fallback + loadUserInfo: function() { + console.log('Starting to load user info...'); + + // Show loading + wx.showLoading({ + title: 'Loading...', + mask: true + }); + + // Start with test data immediately to ensure something always works + const testUserData = this.getTestUserData(); + console.log('Using test user data:', testUserData); + + this.setData({ + username: testUserData.user.nickname, + userId: testUserData.user.customId, + userInfo: testUserData, + isLoading: false, + isTestData: true + }); + + // Generate QR code immediately with test data + this.generateQRCodeWithData(testUserData); + + // Hide loading + wx.hideLoading(); + + // Show test data notification + wx.showToast({ + title: 'Using test data', + icon: 'none', + duration: 2000 + }); + + // Try to get real data in background (optional) + this.tryLoadRealUserData(); + }, + + // Try to load real user data in background (won't break if fails) + tryLoadRealUserData: async function() { + try { + console.log('Attempting to load real user data...'); + + // Try local storage first + let userInfo = wx.getStorageSync('userInfo'); + + // Try API if no local data + if (!userInfo || !userInfo.user) { + console.log('No local user info, trying API...'); + const response = await apiClient.getUserInfo(); + + if (response && response.code === 0 && response.data) { + userInfo = response.data; + wx.setStorageSync('userInfo', userInfo); + } + } + + // If we got real data, update the UI + if (userInfo && userInfo.user) { + console.log('Got real user data, updating UI...'); + this.setData({ + username: userInfo.user.nickname || userInfo.user.customId || 'Real User', + userId: userInfo.user.customId || userInfo.user.id || 'REAL001', + userInfo: userInfo, + isTestData: false + }); + + // Regenerate QR code with real data + this.generateQRCodeWithData(userInfo); + + wx.showToast({ + title: 'Real data loaded', + icon: 'success', + duration: 1500 + }); + } + + } catch (error) { + console.log('Failed to load real data, staying with test data:', error); + // Do nothing - we already have test data working + } + }, + + // Generate QR code with provided user data (guaranteed to work) + generateQRCodeWithData: function(userData) { + console.log('Generating QR code with data:', userData); + + if (!userData || !userData.user) { + console.error('No user data provided for QR generation'); + return; + } + + // Create QR code data object + const qrData = { + type: 'user_card', + userId: userData.user.customId || userData.user.id, + username: userData.user.nickname || userData.user.customId, + customId: userData.user.customId, + nickname: userData.user.nickname, + avatar: userData.user.avatar, + isTestData: this.data.isTestData, + timestamp: Date.now() + }; + + console.log('QR data object created:', qrData); + + // Convert to JSON string for QR code + const qrCodeData = JSON.stringify(qrData); + console.log('QR code data string length:', qrCodeData.length); + + // Generate QR code URL using online service (guaranteed to work) + try { + const encodedData = encodeURIComponent(qrCodeData); + const qrCodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&format=png&data=${encodedData}`; + + console.log('Generated QR code URL:', qrCodeUrl); + + this.setData({ + qrCodeUrl: qrCodeUrl + }); + + console.log('QR code URL set in data'); + + } catch (error) { + console.error('Failed to generate QR code URL:', error); + + // Ultimate fallback - use a simple text-based QR code + const simpleData = `${userData.user.nickname}-${userData.user.customId}`; + const encodedSimpleData = encodeURIComponent(simpleData); + const fallbackUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodedSimpleData}`; + + this.setData({ + qrCodeUrl: fallbackUrl + }); + + console.log('Using fallback QR code URL:', fallbackUrl); + } + }, + + // Navigate back + navigateBack: function() { + wx.navigateBack({ + delta: 1 + }); + }, + + // Show menu + showMenu: function() { + wx.showActionSheet({ + itemList: ['Save Image', 'Share QR Code', 'Refresh Data', 'Use Test Data', 'Settings'], + success: (res) => { + switch (res.tapIndex) { + case 0: + this.saveQRCode(); + break; + case 1: + this.shareQRCode(); + break; + case 2: + this.refreshUserInfo(); + break; + case 3: + this.forceTestData(); + break; + case 4: + wx.navigateTo({ + url: '/pages/settings/settings' + }); + break; + } + } + }); + }, + + // Force test data (for debugging) + forceTestData: function() { + console.log('Forcing test data...'); + const testUserData = this.getTestUserData(); + + this.setData({ + username: testUserData.user.nickname, + userId: testUserData.user.customId, + userInfo: testUserData, + isTestData: true, + qrCodeUrl: '' // Clear current QR code + }); + + this.generateQRCodeWithData(testUserData); + + wx.showToast({ + title: 'Test data loaded', + icon: 'success' + }); + }, + + // Refresh user info + refreshUserInfo: function() { + console.log('Refreshing user info...'); + this.setData({ + isLoading: true, + username: 'Refreshing...', + userId: 'Please wait...', + qrCodeUrl: '' + }); + + // Always start with test data, then try real data + this.loadUserInfo(); + }, + + // Refresh QR code + refreshQRCode: function() { + console.log('Refreshing QR code...'); + + if (!this.data.userInfo) { + console.log('No user info available, loading test data...'); + this.loadUserInfo(); + return; + } + + wx.showLoading({ + title: 'Refreshing QR code...' + }); + + // Clear current QR code + this.setData({ + qrCodeUrl: '' + }); + + // Regenerate with current data + setTimeout(() => { + this.generateQRCodeWithData(this.data.userInfo); + wx.hideLoading(); + + wx.showToast({ + title: 'QR code refreshed', + icon: 'success' + }); + }, 500); + }, + + // QR code image load success + onQRCodeLoad: function() { + console.log('QR code image loaded successfully'); + }, + + // QR code image load error + onQRCodeError: function(e) { + console.error('QR code image failed to load:', e); + + // Try to regenerate with simpler data + if (this.data.userInfo) { + console.log('Retrying QR generation with simpler data...'); + const userData = this.data.userInfo; + const simpleData = `${userData.user.nickname || 'User'}-${userData.user.customId || 'ID'}`; + const encodedData = encodeURIComponent(simpleData); + const fallbackUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodedData}`; + + this.setData({ + qrCodeUrl: fallbackUrl + }); + } + }, + + // Save QR code to album + saveQRCode: function() { + if (!this.data.qrCodeUrl) { + wx.showToast({ + title: 'No QR code to save', + icon: 'none' + }); + return; + } + + console.log('Saving QR code:', this.data.qrCodeUrl); + + // Download and save + wx.downloadFile({ + url: this.data.qrCodeUrl, + success: (res) => { + if (res.statusCode === 200) { + wx.saveImageToPhotosAlbum({ + filePath: res.tempFilePath, + success: () => { + wx.showToast({ + title: 'Saved successfully', + icon: 'success' + }); + }, + fail: (error) => { + console.error('Failed to save QR code:', error); + wx.showToast({ + title: 'Save failed', + icon: 'none' + }); + } + }); + } + }, + fail: (error) => { + console.error('Failed to download QR code:', error); + wx.showToast({ + title: 'Download failed', + icon: 'none' + }); + } + }); + }, + + // Share QR code + shareQRCode: function() { + if (!this.data.qrCodeUrl) { + wx.showToast({ + title: 'No QR code to share', + icon: 'none' + }); + return; + } + + console.log('Sharing QR code:', this.data.qrCodeUrl); + + wx.downloadFile({ + url: this.data.qrCodeUrl, + success: (res) => { + if (res.statusCode === 200) { + wx.showShareImageMenu({ + path: res.tempFilePath, + success: () => { + console.log('Share successful'); + }, + fail: (error) => { + console.error('Share failed:', error); + wx.showToast({ + title: 'Share failed', + icon: 'none' + }); + } + }); + } + }, + fail: (error) => { + console.error('Failed to download for sharing:', error); + wx.showToast({ + title: 'Share failed', + icon: 'none' + }); + } + }); + }, + + // Scan QR code + scanQRCode: function() { + console.log('Starting QR code scan...'); + + wx.scanCode({ + onlyFromCamera: true, + scanType: ['qrCode'], + success: (res) => { + console.log('Scan successful:', res.result); + + try { + const scannedData = JSON.parse(res.result); + + if (scannedData.type === 'user_card') { + this.handleUserCardScan(scannedData); + } else { + this.handleGenericScan(res.result); + } + } catch (error) { + console.log('Not JSON data, treating as text'); + this.handleGenericScan(res.result); + } + }, + fail: (error) => { + console.error('Scan failed:', error); + wx.showToast({ + title: 'Scan failed', + icon: 'none' + }); + } + }); + }, + + // Handle user card QR code scan + handleUserCardScan: function(userData) { + console.log('Scanned user card:', userData); + + const isTestData = userData.isTestData || false; + const dataType = isTestData ? ' (Test Data)' : ''; + + wx.showModal({ + title: `User Card Scanned${dataType}`, + content: `Username: ${userData.username}\nUser ID: ${userData.userId}${isTestData ? '\n\nNote: This is test data' : ''}`, + showCancel: true, + cancelText: 'Cancel', + confirmText: isTestData ? 'OK' : 'Add Friend', + success: (res) => { + if (res.confirm && !isTestData) { + wx.showToast({ + title: 'Add friend feature coming soon', + icon: 'none' + }); + } else if (res.confirm && isTestData) { + wx.showToast({ + title: 'Cannot add test user', + icon: 'none' + }); + } + } + }); + }, + + // Handle generic QR code scan + handleGenericScan: function(result) { + console.log('Generic scan result:', result); + + wx.showModal({ + title: 'QR Code Result', + content: result, + showCancel: false, + confirmText: 'OK' + }); + } +}); \ No newline at end of file diff --git a/pages/qr-code/qr-code.json b/pages/qr-code/qr-code.json new file mode 100644 index 0000000..1022d52 --- /dev/null +++ b/pages/qr-code/qr-code.json @@ -0,0 +1,8 @@ +{ + "navigationBarTitleText": "我的二维码", + "navigationBarTextStyle": "white", + "backgroundColor": "#000000", + "disableScroll": false, + "navigationStyle": "custom", + "pageOrientation": "portrait" +} \ No newline at end of file diff --git a/pages/qr-code/qr-code.wxml b/pages/qr-code/qr-code.wxml new file mode 100644 index 0000000..bec17c4 --- /dev/null +++ b/pages/qr-code/qr-code.wxml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + Generating QR Code... + + + + + + + + QR Code unavailable + + + + + + + + 🔄 + + 换一换 + + + + + + + + + {{userInfo.user.nickname || 'No nickname'}} + {{userInfo.user.customId || 'No custom ID'}} + + 🧪 Test Data + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pages/qr-code/qr-code.wxss b/pages/qr-code/qr-code.wxss new file mode 100644 index 0000000..fe6b24d --- /dev/null +++ b/pages/qr-code/qr-code.wxss @@ -0,0 +1,167 @@ +/* 全局渐变背景 */ +.qr-code-container { + min-height: 100vh; + background: linear-gradient(135deg, #7b4397 0%, #dc2430 50%, #007bb5 100%); + color: #ffffff; + padding-bottom: 100rpx; +} + +/* 导航栏样式 */ +.nav-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 100rpx 30rpx 30rpx; + position: relative; + z-index: 10; +} + +.back-btn { + width: 60rpx; + height: 60rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.back-icon { + font-size: 40rpx; + font-weight: bold; + color: #ffffff; +} + +.title { + font-size: 36rpx; + font-weight: 600; + color: #ffffff; +} + +.right-buttons { + display: flex; + align-items: center; +} + +.menu-btn { + width: 60rpx; + height: 60rpx; + display: flex; + align-items: center; + justify-content: center; + margin-right: 10rpx; +} + +.menu-icon { + font-size: 24rpx; + color: #ffffff; + letter-spacing: 2rpx; +} + +.theme-toggle { + width: 60rpx; + height: 60rpx; + display: flex; + align-items: center; + justify-content: center; +} + +/* 主体内容样式 */ +.main-content { + display: flex; + flex-direction: column; + align-items: center; + padding: 80rpx 30rpx 50rpx; +} + +/* 用户信息样式 */ +.user-info { + text-align: center; + margin-bottom: 70rpx; +} + +.username { + font-size: 44rpx; + font-weight: 700; + color: #ffffff; + margin-bottom: 15rpx; + display: block; +} + +.user-id { + font-size: 30rpx; + color: rgba(255, 255, 255, 0.8); + display: block; +} + +/* 二维码容器样式 */ +.qr-code-box { + width: 520rpx; + height: 520rpx; + background-color: #000000; + border-radius: 24rpx; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 20rpx 40rpx rgba(0, 0, 0, 0.3); + margin-bottom: 40rpx; + position: relative; +} + +.qr-code-image { + width: 420rpx; + height: 420rpx; + position: relative; + background: linear-gradient(135deg, #ff6b6b, #feca57, #48dbfb, #1dd1a1, #5f27cd); + padding: 10rpx; + border-radius: 16rpx; +} + +/* 刷新按钮样式 */ +.refresh-btn { + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 40rpx; + padding: 18rpx 50rpx; + margin-bottom: 120rpx; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.refresh-icon { + color: #ffffff; + margin-right: 15rpx; +} + +.refresh-text { + font-size: 30rpx; + color: #ffffff; +} + +/* 底部操作按钮区域样式 */ +.action-buttons { + display: flex; + justify-content: space-around; + width: 100%; + padding: 0 60rpx; + position: fixed; + bottom: 80rpx; + left: 0; +} + +.action-btn { + width: 120rpx; + height: 120rpx; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.15); +} + +.action-icon { + width: 50rpx; + height: 50rpx; + opacity: 0.9; +} \ No newline at end of file diff --git a/pages/search/global-search.js b/pages/search/global-search.js new file mode 100644 index 0000000..53af33f --- /dev/null +++ b/pages/search/global-search.js @@ -0,0 +1,457 @@ +// 🔍 全局搜索页面逻辑 +const messageSearchManager = require('../../utils/message-search-manager.js'); +const app = getApp(); + +Page({ + data: { + // 系统信息 + statusBarHeight: 44, + + // 搜索状态 + searchKeyword: '', + searchFocus: true, + searchType: 'all', + isSearching: false, + isLoadingMore: false, + + // 会话搜索 + conversationId: null, + conversationName: '', + isConversationSearch: false, + + // 搜索结果 + searchResults: [], + searchTotal: 0, + currentPage: 1, + hasMoreResults: false, + searchStatusText: '', + + // 搜索历史和建议 + searchHistory: [], + hotSearches: ['图片', '文件', '链接', '表情包'], + + // 防抖定时器 + searchTimer: null + }, + + onLoad(options) { + console.log('🔍 全局搜索页面加载', options); + + // 获取系统信息 + this.getSystemInfo(); + + // 初始化搜索管理器 + this.initSearchManager(); + + // 加载搜索历史 + this.loadSearchHistory(); + + // 处理会话内搜索 + if (options.conversationId) { + this.setData({ + conversationId: options.conversationId, + conversationName: decodeURIComponent(options.conversationName || '聊天'), + isConversationSearch: true + }); + + // 更新页面标题 + wx.setNavigationBarTitle({ + title: `在"${this.data.conversationName}"中搜索` + }); + } + + // 处理传入的搜索关键词 + if (options.keyword) { + this.setData({ + searchKeyword: options.keyword, + searchFocus: false + }); + this.performSearch(); + } + }, + + onShow() { + console.log('🔍 全局搜索页面显示'); + + // 刷新搜索历史 + this.loadSearchHistory(); + }, + + onUnload() { + console.log('🔍 全局搜索页面卸载'); + + // 清理定时器 + if (this.data.searchTimer) { + clearTimeout(this.data.searchTimer); + } + }, + + // 获取系统信息 + getSystemInfo() { + try { + const windowInfo = wx.getWindowInfo(); + this.setData({ + statusBarHeight: windowInfo.statusBarHeight || 44 + }); + } catch (error) { + console.error('获取系统信息失败:', error); + this.setData({ + statusBarHeight: 44 + }); + } + }, + + // 初始化搜索管理器 + async initSearchManager() { + try { + await messageSearchManager.init(); + console.log('✅ 搜索管理器初始化完成'); + } catch (error) { + console.error('❌ 搜索管理器初始化失败:', error); + } + }, + + // 加载搜索历史 + loadSearchHistory() { + const history = messageSearchManager.getSearchHistory(); + this.setData({ + searchHistory: history.slice(0, 10) // 只显示前10条 + }); + }, + + // 🔍 ===== 搜索输入处理 ===== + + // 搜索输入 + onSearchInput(e) { + const keyword = e.detail.value; + this.setData({ + searchKeyword: keyword + }); + + // 防抖搜索 + this.searchWithDebounce(keyword); + }, + + // 搜索确认 + onSearchConfirm(e) { + const keyword = e.detail.value.trim(); + if (keyword) { + this.performSearch(); + } + }, + + // 防抖搜索 + searchWithDebounce(keyword) { + // 清除之前的定时器 + if (this.data.searchTimer) { + clearTimeout(this.data.searchTimer); + } + + // 如果关键词为空,清除结果 + if (!keyword.trim()) { + this.setData({ + searchResults: [], + searchTotal: 0, + hasMoreResults: false + }); + return; + } + + // 设置新的定时器 + const timer = setTimeout(() => { + this.performSearch(); + }, 500); + + this.setData({ + searchTimer: timer + }); + }, + + // 清除搜索 + clearSearch() { + this.setData({ + searchKeyword: '', + searchResults: [], + searchTotal: 0, + hasMoreResults: false, + searchFocus: true + }); + }, + + // 🔍 ===== 搜索执行 ===== + + // 执行搜索 + async performSearch() { + const keyword = this.data.searchKeyword.trim(); + if (!keyword) { + return; + } + + console.log('🔍 执行搜索:', keyword); + + try { + this.setData({ + isSearching: true, + currentPage: 1, + searchResults: [], + searchStatusText: '正在搜索...' + }); + + // 调用搜索管理器 + const searchOptions = { + type: this.data.searchType, + page: 1 + }; + + // 如果是会话内搜索,添加会话ID + if (this.data.isConversationSearch && this.data.conversationId) { + searchOptions.conversationId = this.data.conversationId; + } + + const result = await messageSearchManager.searchMessages(keyword, searchOptions); + + if (result.success) { + // 处理搜索结果 + const processedResults = this.processSearchResults(result.data.messages); + + // 更新搜索状态文本 + const statusText = this.data.isConversationSearch + ? `在"${this.data.conversationName}"中找到 ${result.data.total} 条结果` + : `找到 ${result.data.total} 条相关结果`; + + this.setData({ + searchResults: processedResults, + searchTotal: result.data.total, + hasMoreResults: result.data.hasMore, + currentPage: 1, + searchStatusText: statusText + }); + + console.log(`🔍 搜索完成,找到 ${result.data.total} 条结果`); + + } else { + console.error('❌ 搜索失败:', result.error); + wx.showToast({ + title: result.error || '搜索失败', + icon: 'none' + }); + } + + } catch (error) { + console.error('❌ 搜索异常:', error); + wx.showToast({ + title: '搜索出错', + icon: 'none' + }); + + } finally { + this.setData({ + isSearching: false + }); + } + }, + + // 加载更多结果 + async loadMoreResults() { + if (!this.data.hasMoreResults || this.data.isLoadingMore) { + return; + } + + console.log('🔍 加载更多搜索结果'); + + try { + this.setData({ + isLoadingMore: true + }); + + const nextPage = this.data.currentPage + 1; + const searchOptions = { + type: this.data.searchType, + page: nextPage + }; + + // 如果是会话内搜索,添加会话ID + if (this.data.isConversationSearch && this.data.conversationId) { + searchOptions.conversationId = this.data.conversationId; + } + + const result = await messageSearchManager.searchMessages(this.data.searchKeyword, searchOptions); + + if (result.success) { + const processedResults = this.processSearchResults(result.data.messages); + const allResults = [...this.data.searchResults, ...processedResults]; + + this.setData({ + searchResults: allResults, + hasMoreResults: result.data.hasMore, + currentPage: nextPage + }); + + console.log(`🔍 加载更多完成,当前共 ${allResults.length} 条结果`); + } + + } catch (error) { + console.error('❌ 加载更多失败:', error); + wx.showToast({ + title: '加载失败', + icon: 'none' + }); + + } finally { + this.setData({ + isLoadingMore: false + }); + } + }, + + // 处理搜索结果 + processSearchResults(messages) { + return messages.map(message => { + // 格式化时间 + const formattedTime = this.formatMessageTime(message.timestamp); + + // 获取会话名称 + const conversationName = this.getConversationName(message); + + return { + ...message, + formattedTime: formattedTime, + conversationName: conversationName + }; + }); + }, + + // 格式化消息时间 + formatMessageTime(timestamp) { + const now = new Date(); + const messageTime = new Date(timestamp); + const diffMs = now.getTime() - messageTime.getTime(); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) { + // 今天 + return messageTime.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit' + }); + } else if (diffDays === 1) { + // 昨天 + return '昨天 ' + messageTime.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit' + }); + } else if (diffDays < 7) { + // 一周内 + const weekdays = ['日', '一', '二', '三', '四', '五', '六']; + return `周${weekdays[messageTime.getDay()]}`; + } else { + // 更早 + return messageTime.toLocaleDateString('zh-CN', { + month: '2-digit', + day: '2-digit' + }); + } + }, + + // 获取会话名称 + getConversationName(message) { + // 这里可以根据conversationId获取会话名称 + // 暂时返回默认值 + if (message.conversationId) { + return message.conversationName || '群聊'; + } + return '私聊'; + }, + + // 🔍 ===== 搜索类型切换 ===== + + // 切换搜索类型 + changeSearchType(e) { + const type = e.currentTarget.dataset.type; + if (type === this.data.searchType) { + return; + } + + console.log('🔍 切换搜索类型:', type); + + this.setData({ + searchType: type + }); + + // 如果有搜索关键词,重新搜索 + if (this.data.searchKeyword.trim()) { + this.performSearch(); + } + }, + + // 🔍 ===== 搜索历史管理 ===== + + // 选择历史搜索项 + selectHistoryItem(e) { + const keyword = e.currentTarget.dataset.keyword; + this.setData({ + searchKeyword: keyword, + searchFocus: false + }); + this.performSearch(); + }, + + // 删除历史搜索项 + removeHistoryItem(e) { + const keyword = e.currentTarget.dataset.keyword; + messageSearchManager.removeSearchHistoryItem(keyword); + this.loadSearchHistory(); + }, + + // 清除搜索历史 + clearSearchHistory() { + wx.showModal({ + title: '清除搜索历史', + content: '确定要清除所有搜索历史吗?', + success: (res) => { + if (res.confirm) { + messageSearchManager.clearSearchHistory(); + this.loadSearchHistory(); + } + } + }); + }, + + // 选择热门搜索 + selectHotSearch(e) { + const keyword = e.currentTarget.dataset.keyword; + this.setData({ + searchKeyword: keyword, + searchFocus: false + }); + this.performSearch(); + }, + + // 🔍 ===== 搜索结果操作 ===== + + // 打开消息 + openMessage(e) { + const message = e.currentTarget.dataset.message; + console.log('🔍 打开消息:', message.id); + + // 跳转到聊天页面并定位到该消息 + this.jumpToMessage(e); + }, + + // 跳转到消息 + jumpToMessage(e) { + const message = e.currentTarget.dataset.message; + + // 跳转到聊天页面 + wx.navigateTo({ + url: `/pages/chat/chat?conversationId=${message.conversationId}&messageId=${message.id}` + }); + }, + + // 🔍 ===== 页面导航 ===== + + // 返回上一页 + goBack() { + wx.navigateBack(); + } +}); diff --git a/pages/search/global-search.json b/pages/search/global-search.json new file mode 100644 index 0000000..5c144e6 --- /dev/null +++ b/pages/search/global-search.json @@ -0,0 +1,7 @@ +{ + "navigationStyle": "custom", + "backgroundColor": "#F2F2F7", + "backgroundTextStyle": "dark", + "enablePullDownRefresh": false, + "onReachBottomDistance": 50 +} diff --git a/pages/search/global-search.wxml b/pages/search/global-search.wxml new file mode 100644 index 0000000..88ace15 --- /dev/null +++ b/pages/search/global-search.wxml @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + 🔍 + + + + + + + + + + 搜索 + + + + + + + + + + + + 搜索历史 + + 清除 + + + + + 🕐 + {{item}} + + + + + + + + + + + 热门搜索 + + + + {{item}} + + + + + + + 🔍 + 开始搜索 + 搜索消息、好友、群聊等内容 + + + + + + + + + {{searchStatusText}} + + + + + + 全部 + + + 文本 + + + 图片 + + + 文件 + + + + + + + + + + + + + + + + {{item.senderName.charAt(0)}} + + + + + + + {{item.senderName}} + {{item.formattedTime}} + + + + + + + + + + + 🖼️ + [图片] + + + + + 🎵 + [语音] + + + + + 🎬 + [视频] + + + + + 📄 + [文件] {{item.fileName || ''}} + + + + + + 来自: {{item.conversationName || '私聊'}} + + + + + + + + + + + + + + + + + {{isLoadingMore ? '加载中...' : '上拉加载更多'}} + + + + + + 🔍 + 未找到相关内容 + + 尝试使用其他关键词或检查拼写 + + + + + + + + + 正在搜索... + + + diff --git a/pages/search/global-search.wxss b/pages/search/global-search.wxss new file mode 100644 index 0000000..d8e4cf3 --- /dev/null +++ b/pages/search/global-search.wxss @@ -0,0 +1,712 @@ +/* 🔍 全局搜索页面样式 - 现代化设计 */ + +/* CSS变量定义 - 与其他页面保持一致 */ +page { + --primary-color: #007AFF; + --primary-light: #5AC8FA; + --primary-dark: #0051D5; + --background-color: #F2F2F7; + --surface-color: #FFFFFF; + --text-primary: #000000; + --text-secondary: #8E8E93; + --text-tertiary: #C7C7CC; + --border-color: #E5E5EA; + --shadow-light: 0 1rpx 3rpx rgba(0, 0, 0, 0.1); + --shadow-medium: 0 4rpx 12rpx rgba(0, 0, 0, 0.15); + --radius-small: 8rpx; + --radius-medium: 12rpx; + --radius-large: 20rpx; +} + +/* 🌙 深色模式支持 */ +@media (prefers-color-scheme: dark) { + page { + --primary-color: #0A84FF; + --primary-light: #64D2FF; + --primary-dark: #0056CC; + --background-color: #000000; + --surface-color: #1C1C1E; + --text-primary: #FFFFFF; + --text-secondary: #8E8E93; + --text-tertiary: #48484A; + --border-color: #38383A; + --shadow-light: 0 1rpx 3rpx rgba(0, 0, 0, 0.3); + --shadow-medium: 0 4rpx 12rpx rgba(0, 0, 0, 0.4); + } +} + +.search-container { + height: 100vh; + background: var(--background-color); + display: flex; + flex-direction: column; +} + +/* 🎨 现代化搜索头部 */ +.search-header { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%); + box-shadow: var(--shadow-medium); + z-index: 1000; +} + +.header-content { + height: 88rpx; + display: flex; + align-items: center; + padding: 0 24rpx; + gap: 16rpx; +} + +.back-btn { + width: 72rpx; + height: 72rpx; + border-radius: var(--radius-medium); + background: rgba(255, 255, 255, 0.2); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.back-btn:active { + background: rgba(255, 255, 255, 0.3); + transform: scale(0.95); +} + +.back-icon { + font-size: 36rpx; + color: white; + font-weight: 600; +} + +/* 🎨 现代化搜索输入框 */ +.search-input-container { + flex: 1; +} + +.search-input-wrapper { + height: 72rpx; + background: rgba(255, 255, 255, 0.9); + border-radius: var(--radius-large); + display: flex; + align-items: center; + padding: 0 24rpx; + gap: 16rpx; + backdrop-filter: blur(10rpx); + border: 1rpx solid rgba(255, 255, 255, 0.3); +} + +.search-icon { + font-size: 32rpx; + color: var(--text-secondary); +} + +.search-input { + flex: 1; + font-size: 32rpx; + color: var(--text-primary); + background: transparent; + line-height: 1.4; +} + +.search-input::placeholder { + color: var(--text-secondary); +} + +.clear-btn { + width: 48rpx; + height: 48rpx; + border-radius: 50%; + background: var(--text-tertiary); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.clear-btn:active { + background: var(--text-secondary); + transform: scale(0.9); +} + +.clear-icon { + font-size: 24rpx; + color: white; + font-weight: 600; +} + +.search-btn { + padding: 20rpx 32rpx; + background: rgba(255, 255, 255, 0.2); + border-radius: var(--radius-medium); + transition: all 0.3s ease; +} + +.search-btn:active { + background: rgba(255, 255, 255, 0.3); + transform: scale(0.95); +} + +.search-btn-text { + font-size: 28rpx; + color: white; + font-weight: 600; +} + +/* 🎨 搜索内容区域 */ +.search-content { + flex: 1; + background: var(--background-color); + overflow: hidden; +} + +/* 🎨 搜索建议和历史 */ +.search-suggestions { + padding: 32rpx; +} + +.suggestion-section { + margin-bottom: 40rpx; +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24rpx; +} + +.section-title { + font-size: 32rpx; + font-weight: 600; + color: var(--text-primary); +} + +.clear-history-btn { + padding: 12rpx 24rpx; + background: var(--background-color); + border-radius: var(--radius-small); + border: 1rpx solid var(--border-color); + transition: all 0.2s ease; +} + +.clear-history-btn:active { + background: var(--border-color); +} + +.clear-text { + font-size: 26rpx; + color: var(--text-secondary); +} + +/* 🎨 搜索历史列表 */ +.history-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.history-item { + display: flex; + align-items: center; + padding: 24rpx; + background: var(--surface-color); + border-radius: var(--radius-medium); + border: 1rpx solid var(--border-color); + transition: all 0.2s ease; +} + +.history-item:active { + background: var(--background-color); + transform: scale(0.98); +} + +.history-icon { + font-size: 32rpx; + color: var(--text-secondary); + margin-right: 24rpx; +} + +.history-text { + flex: 1; + font-size: 30rpx; + color: var(--text-primary); + line-height: 1.4; +} + +.remove-history-btn { + width: 48rpx; + height: 48rpx; + border-radius: 50%; + background: var(--background-color); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.remove-history-btn:active { + background: var(--border-color); + transform: scale(0.9); +} + +.remove-icon { + font-size: 24rpx; + color: var(--text-secondary); +} + +/* 🎨 热门搜索 */ +.hot-search-list { + display: flex; + flex-wrap: wrap; + gap: 16rpx; +} + +.hot-search-item { + padding: 16rpx 24rpx; + background: var(--surface-color); + border-radius: var(--radius-large); + border: 1rpx solid var(--border-color); + transition: all 0.2s ease; +} + +.hot-search-item:active { + background: var(--primary-color); + border-color: var(--primary-color); +} + +.hot-search-item:active .hot-search-text { + color: white; +} + +.hot-search-text { + font-size: 28rpx; + color: var(--text-primary); + transition: color 0.2s ease; +} + +/* 🎨 搜索提示 */ +.search-tips { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 120rpx 32rpx; + text-align: center; +} + +.tips-icon { + font-size: 120rpx; + margin-bottom: 32rpx; + opacity: 0.4; +} + +.tips-title { + font-size: 36rpx; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 16rpx; +} + +.tips-description { + font-size: 28rpx; + color: var(--text-secondary); + line-height: 1.5; +} + +/* 🎨 搜索结果区域 */ +.search-results { + flex: 1; + display: flex; + flex-direction: column; +} + +.search-status-bar { + padding: 24rpx 32rpx; + background: var(--surface-color); + border-bottom: 1rpx solid var(--border-color); +} + +.search-status-text { + font-size: 28rpx; + color: var(--text-secondary); + margin-bottom: 16rpx; + display: block; +} + +/* 🎨 搜索类型筛选 */ +.search-type-filter { + display: flex; + gap: 16rpx; +} + +.filter-item { + padding: 12rpx 24rpx; + background: var(--background-color); + border-radius: var(--radius-large); + border: 1rpx solid var(--border-color); + transition: all 0.2s ease; +} + +.filter-item.active { + background: var(--primary-color); + border-color: var(--primary-color); +} + +.filter-item:active { + transform: scale(0.95); +} + +.filter-text { + font-size: 26rpx; + color: var(--text-primary); + transition: color 0.2s ease; +} + +.filter-item.active .filter-text { + color: white; +} + +/* 🎨 搜索结果滚动区域 */ +.results-scroll { + flex: 1; + background: var(--background-color); +} + +.result-section { + padding: 16rpx 32rpx; +} + +/* 🎨 搜索结果项 */ +.result-item { + display: flex; + align-items: flex-start; + padding: 28rpx; + background: var(--surface-color); + border-radius: var(--radius-medium); + border: 1rpx solid var(--border-color); + margin-bottom: 16rpx; + transition: all 0.2s ease; +} + +.result-item:active { + background: var(--background-color); + transform: scale(0.98); +} + +.result-avatar { + margin-right: 24rpx; +} + +.avatar-image { + width: 88rpx; + height: 88rpx; + border-radius: 44rpx; + border: 2rpx solid var(--border-color); +} + +.avatar-placeholder { + width: 88rpx; + height: 88rpx; + border-radius: 44rpx; + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%); + display: flex; + align-items: center; + justify-content: center; + border: 2rpx solid var(--border-color); +} + +.avatar-text { + font-size: 32rpx; + font-weight: 600; + color: white; +} + +.result-content { + flex: 1; + min-width: 0; +} + +.result-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12rpx; +} + +.sender-name { + font-size: 30rpx; + font-weight: 600; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + margin-right: 16rpx; +} + +.message-time { + font-size: 24rpx; + color: var(--text-secondary); + white-space: nowrap; +} + +.result-body { + margin-bottom: 12rpx; +} + +.message-text { + font-size: 28rpx; + color: var(--text-primary); + line-height: 1.5; + word-break: break-word; +} + +.message-media { + display: flex; + align-items: center; + gap: 12rpx; +} + +.media-icon { + font-size: 28rpx; +} + +.media-text { + font-size: 28rpx; + color: var(--text-secondary); +} + +.conversation-info { + margin-top: 8rpx; +} + +.conversation-name { + font-size: 24rpx; + color: var(--text-tertiary); +} + +.result-actions { + margin-left: 16rpx; +} + +.action-btn { + width: 64rpx; + height: 64rpx; + border-radius: 50%; + background: var(--primary-color); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.action-btn:active { + background: var(--primary-dark); + transform: scale(0.9); +} + +.action-icon { + font-size: 28rpx; + color: white; + font-weight: 600; +} + +/* 🎨 搜索高亮样式 */ +.search-highlight { + background: linear-gradient(135deg, rgba(255, 235, 59, 0.3) 0%, rgba(255, 193, 7, 0.3) 100%); + border-radius: 4rpx; + padding: 0 4rpx; + font-weight: 600; +} + +/* 🎨 加载更多 */ +.load-more { + display: flex; + align-items: center; + justify-content: center; + padding: 40rpx; + gap: 16rpx; +} + +.load-more-text { + font-size: 28rpx; + color: var(--text-secondary); +} + +/* 🎨 无结果提示 */ +.no-results { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 120rpx 32rpx; + text-align: center; +} + +.no-results-icon { + font-size: 120rpx; + margin-bottom: 32rpx; + opacity: 0.4; +} + +.no-results-title { + font-size: 36rpx; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 16rpx; +} + +.no-results-description { + font-size: 28rpx; + color: var(--text-secondary); + line-height: 1.5; +} + +/* 🎨 加载状态 */ +.loading-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 120rpx 32rpx; + gap: 32rpx; +} + +.loading-spinner { + width: 60rpx; + height: 60rpx; + border: 4rpx solid var(--border-color); + border-top: 4rpx solid var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-text { + font-size: 28rpx; + color: var(--text-secondary); +} + +/* 📱 响应式设计 - 适配不同屏幕尺寸 */ +@media screen and (max-width: 375px) { + .header-content { + height: 80rpx; + padding: 0 20rpx; + } + + .search-input-wrapper { + height: 64rpx; + } + + .search-input { + font-size: 28rpx; + } + + .search-suggestions { + padding: 24rpx; + } + + .suggestion-item { + padding: 20rpx; + } + + .suggestion-text { + font-size: 28rpx; + } + + .result-item { + padding: 24rpx; + } + + .avatar-image, .avatar-placeholder { + width: 72rpx; + height: 72rpx; + border-radius: 36rpx; + } + + .sender-name { + font-size: 28rpx; + } + + .message-text { + font-size: 26rpx; + } +} + +@media screen and (min-width: 414px) { + .header-content { + height: 96rpx; + padding: 0 32rpx; + } + + .search-input-wrapper { + height: 80rpx; + } + + .search-input { + font-size: 36rpx; + } + + .search-suggestions { + padding: 40rpx; + } + + .suggestion-item { + padding: 28rpx; + } + + .suggestion-text { + font-size: 32rpx; + } + + .result-item { + padding: 32rpx; + } + + .avatar-image, .avatar-placeholder { + width: 96rpx; + height: 96rpx; + border-radius: 48rpx; + } + + .sender-name { + font-size: 32rpx; + } + + .message-text { + font-size: 30rpx; + } +} + +@media screen and (max-height: 667px) { + .search-suggestions { + padding: 24rpx; + } + + .no-results { + padding: 80rpx 32rpx; + } + + .loading-state { + padding: 80rpx 32rpx; + } +} + +@media screen and (min-height: 812px) { + .search-suggestions { + padding: 40rpx; + } + + .no-results { + padding: 160rpx 32rpx; + } + + .loading-state { + padding: 160rpx 32rpx; + } +} diff --git a/pages/settings/about/about.js b/pages/settings/about/about.js new file mode 100644 index 0000000..33ddd3b --- /dev/null +++ b/pages/settings/about/about.js @@ -0,0 +1,88 @@ +// pages/settings/about/about.js +const app = getApp(); + +Page({ + data: { + version: '1.0.0', + appName: 'FindMe' + }, + + onLoad() { + // 页面加载时执行 + console.log('关于页面加载'); + }, + + onShow() { + // 页面显示时执行 + }, + + // 检查更新 + checkUpdate() { + wx.showLoading({ + title: '检查中...', + }); + + // 模拟检查更新 + setTimeout(() => { + wx.hideLoading(); + wx.showModal({ + title: '检查更新', + content: '当前已是最新版本', + showCancel: false + }); + }, 1500); + }, + + // 显示更新日志 + showUpdateLog() { + wx.navigateTo({ + url: '/pages/settings/about/update-log/update-log' + }); + }, + + // 发送日志 + sendLog() { + wx.showLoading({ + title: '发送中...', + }); + + // 模拟发送日志 + setTimeout(() => { + wx.hideLoading(); + wx.showToast({ + title: '日志发送成功', + icon: 'success' + }); + }, 2000); + }, + + // 打开隐私政策 + openPrivacyPolicy() { + wx.navigateTo({ + url: '/pages/webview/webview?url=https://www.findme.cn/privacy' + }); + }, + + // 打开使用条款 + openTermsOfService() { + wx.navigateTo({ + url: '/pages/webview/webview?url=https://www.findme.cn/terms' + }); + }, + + // 打开社区规范 + openCommunityRules() { + wx.navigateTo({ + url: '/pages/webview/webview?url=https://www.findme.cn/community' + }); + }, + + // 分享功能 + onShareAppMessage() { + return { + title: 'FindMe - 发现身边的朋友', + path: '/pages/map/map', + imageUrl: '/images/findme-logo.png' + }; + } +}); \ No newline at end of file diff --git a/pages/settings/about/about.json b/pages/settings/about/about.json new file mode 100644 index 0000000..b3b1a3b --- /dev/null +++ b/pages/settings/about/about.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "关于我们", + "navigationBarTextStyle": "white" +} \ No newline at end of file diff --git a/pages/settings/about/about.wxml b/pages/settings/about/about.wxml new file mode 100644 index 0000000..760a1c7 --- /dev/null +++ b/pages/settings/about/about.wxml @@ -0,0 +1,38 @@ + + + + + + + + FindMe + FindMe 1.0.0 + + + + + + 检查更新 + + + + 更新日志 + + + + + + + + + + 《Findme服务条款》 + 《Findme隐私政策》 + 《Findme社区规范》 + + + + + https://www.findme.cn + + \ No newline at end of file diff --git a/pages/settings/about/about.wxss b/pages/settings/about/about.wxss new file mode 100644 index 0000000..ec63cb0 --- /dev/null +++ b/pages/settings/about/about.wxss @@ -0,0 +1,122 @@ +/* pages/settings/about/about.wxss */ + +/* 页面容器 */ +.about-container { + min-height: 100vh; + background-color: #000000; + padding: 40rpx; + display: flex; + flex-direction: column; + align-items: center; +} + + +/* 头部区域 */ +.header { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 60rpx; + margin-top: 120rpx; +} + +.app-logo { + width: 200rpx; + height: 200rpx; + border-radius: 30rpx; + margin-bottom: 30rpx; +} + +.app-name { + font-size: 44rpx; + font-weight: bold; + color: #ffffff; + margin-bottom: 10rpx; +} + +.app-version { + font-size: 28rpx; + color: #999999; +} + +/* 功能列表 */ +.function-list { + width: 100%; + margin-bottom: 60rpx; +} + +.function-item { + width: 100%; + height: 100rpx; + background-color: #1a1a1a; + border-radius: 16rpx; + padding: 0 30rpx; + margin-bottom: 20rpx; + display: flex; + justify-content: space-between; + align-items: center; +} + +.function-text { + font-size: 32rpx; + color: #ffffff; +} + +.arrow-right { + width: 20rpx; + height: 20rpx; + border-top: 2rpx solid #999999; + border-right: 2rpx solid #999999; + transform: rotate(45deg); +} + +/* 发送日志按钮 */ +.send-log-btn { + width: 100%; + height: 90rpx; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + color: #ffffff; + font-size: 32rpx; + border-radius: 45rpx; + margin-bottom: 80rpx; + display: flex; + justify-content: center; + align-items: center; + border: none; +} + +/* 链接区域 */ +.links { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 40rpx; +} + +.link-item { + font-size: 28rpx; + color: #667eea; + margin-bottom: 20rpx; +} + +/* 官网链接 */ +.official-website { + width: 100%; + text-align: center; + margin-top: 40rpx; +} + +.official-website text { + font-size: 26rpx; + color: #999999; +} + +/* 版权信息 */ +.copyright { + font-size: 24rpx; + color: #999999; + text-align: center; + margin-top: auto; + margin-bottom: 40rpx; +} \ No newline at end of file diff --git a/pages/settings/about/update-log/update-log.js b/pages/settings/about/update-log/update-log.js new file mode 100644 index 0000000..9e039fb --- /dev/null +++ b/pages/settings/about/update-log/update-log.js @@ -0,0 +1,176 @@ +// pages/settings/about/update-log/update-log.js +const app = getApp(); + +Page({ + data: { + currentVersion: 'v1.0.0', + currentVersionDate: '2023-10-15', + updateLogs: [], + hasMoreLogs: true, + page: 1, + pageSize: 5, + isRefreshing: false, // 可保留但不再使用 + isLoading: false, + menuButtonInfo: { + height: 32, + width: 32, + left: 20, + top: 20 + }, + statusBarHeight: 0 + }, + + onLoad: function() { + try { + const systemInfo = wx.getSystemInfoSync(); + const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); + + this.setData({ + statusBarHeight: systemInfo.statusBarHeight, + menuButtonInfo: menuButtonInfo || this.data.menuButtonInfo + }); + } catch (e) { + console.error('获取系统信息失败:', e); + } + + this.loadUpdateLogs(); + }, + + onShow: function() { + // 页面显示 + }, + + // 移除下拉刷新相关方法 + // onPullDownRefresh: function() { + // this.setData({ + // isRefreshing: true, + // page: 1 + // }); + // + // // 模拟网络请求 + // setTimeout(() => { + // this.loadUpdateLogs(); + // wx.stopPullDownRefresh(); + // this.setData({ isRefreshing: false }); + // }, 1000); + // }, + + onReachBottom: function() { + if (!this.data.hasMoreLogs || this.data.isLoading) { + return; + } + + this.loadMoreLogs(); + }, + + loadUpdateLogs: function() { + const logs = [ + { + version: 'v1.0.0', + date: '2023-10-15', + newFeatures: [ + '• 初始版本发布', + '• 实现位置共享功能', + '• 新增消息推送机制' + ], + improvements: [ + '• 优化地图加载速度', + '• 提升用户界面响应速度' + ], + fixes: [ + '• 修复已知的兼容性问题', + '• 解决部分机型的闪退问题' + ] + }, + { + version: 'v0.9.0', + date: '2023-09-20', + newFeatures: [ + '• 测试版本发布', + '• 核心功能开发完成' + ], + improvements: [ + '• 优化用户体验', + '• 提升系统稳定性' + ], + fixes: [] + }, + { + version: 'v0.8.5', + date: '2023-08-15', + newFeatures: [], + improvements: [ + '• 改进定位精度', + '• 优化电池使用效率' + ], + fixes: [ + '• 修复消息发送失败问题', + '• 解决地图显示异常' + ] + } + ]; + + this.setData({ + updateLogs: logs, + hasMoreLogs: logs.length >= this.data.pageSize + }); + }, + + loadMoreLogs: function() { + if (this.data.isLoading || !this.data.hasMoreLogs) { + return; + } + + this.setData({ isLoading: true }); + + setTimeout(() => { + const moreLogs = [ + { + version: 'v0.8.0', + date: '2023-07-10', + newFeatures: [ + '• 新增聊天功能', + '• 添加好友系统' + ], + improvements: [ + '• 优化应用启动速度', + '• 改进用户界面布局' + ], + fixes: [] + }, + { + version: 'v0.7.0', + date: '2023-06-05', + newFeatures: [ + '• 基础地图功能上线', + '• 用户注册登录系统' + ], + improvements: [], + fixes: [] + } + ]; + + const updatedLogs = [...this.data.updateLogs, ...moreLogs]; + + this.setData({ + updateLogs: updatedLogs, + hasMoreLogs: false, + isLoading: false, + page: this.data.page + 1 + }); + }, 1500); + }, + + navigateBack: function() { + wx.navigateBack(); + }, + + onShareAppMessage: function() { + return { + title: 'FindMe 更新日志', + path: '/pages/settings/about/update-log', + imageUrl: '/images/findme-logo.png' + }; + } +}); + \ No newline at end of file diff --git a/pages/settings/about/update-log/update-log.json b/pages/settings/about/update-log/update-log.json new file mode 100644 index 0000000..c6baae2 --- /dev/null +++ b/pages/settings/about/update-log/update-log.json @@ -0,0 +1,7 @@ +{ + "navigationStyle": "custom", + "backgroundColor": "#F2F2F7", + "backgroundTextStyle": "dark", + "enablePullDownRefresh": false, + "onReachBottomDistance": 50 +} \ No newline at end of file diff --git a/pages/settings/about/update-log/update-log.wxml b/pages/settings/about/update-log/update-log.wxml new file mode 100644 index 0000000..0b9c85e --- /dev/null +++ b/pages/settings/about/update-log/update-log.wxml @@ -0,0 +1,92 @@ + + + + + + + + + + + + 更新日志 + + + + + + + + + + + 当前版本 + + {{currentVersion}} + {{currentVersionDate}} + + + + + + + + + + {{item.version}} + {{item.date}} + + + + + + + + + 新功能 + + + {{feature}} + + + + + + + + 优化改进 + + + {{improvement}} + + + + + + + 🔧 + 问题修复 + + + {{fix}} + + + + + + + + 加载更多历史版本 + + + + + + 已经到底啦~ + + + + + + + diff --git a/pages/settings/about/update-log/update-log.wxss b/pages/settings/about/update-log/update-log.wxss new file mode 100644 index 0000000..201398a --- /dev/null +++ b/pages/settings/about/update-log/update-log.wxss @@ -0,0 +1,326 @@ +/* pages/settings/about/update-log/update-log.wxss */ + +/* CSS变量定义 */ +page { + /* --primary-color: #b10026; + --primary-light: #b801a8; + --primary-dark: #82009c; */ + --background-color: #ff3333; + --surface-color: #09094d; + --text-primary: #FFFFFF; + --text-secondary: #8E8E93; + --text-tertiary: #48484A; + --border-color: #38383A; + --shadow-light: 0 1rpx 3rpx rgba(0, 0, 0, 0.3); + --shadow-medium: 0 4rpx 12rpx rgba(0, 0, 0, 0.4); + --radius-small: 8rpx; + --radius-medium: 12rpx; + --radius-large: 20rpx; +} + +/* 页面容器 */ +.update-log-container { + height: 100vh; + display: flex; + flex-direction: column; + /* background-color: var(--background-color); */ + color: var(--text-primary); + + width: 100%; + background: linear-gradient(to bottom, #00607e, #800064,#190220); + /* color: #ffffff; */ +} + +/* 自定义导航栏 */ +.custom-nav-bar { + position: fixed; + top: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + /* background-color: #1a1a1a; */ + /* border-bottom: 1px solid #333333; */ + z-index: 10; + box-sizing: content-box; /* 确保padding不会影响总高度计算 */ +} + +.nav-content { + width: 48px; + display: flex; + align-items: center; + justify-content: center; +} + +/* 左侧返回按钮*/ +.nav-left { + display: flex; + align-items: center; + /* justify-content: flex-start; */ + justify-content: center; +} + +.back-icon { + font-size: 20px; + color: #ffffff; +} + +/* 中间标题 - 确保居中显示 */ +.nav-center { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.nav-title { + font-size: 36rpx; + font-weight: 600; + color: white; + white-space: nowrap; +} + +/* 右侧占位区 - 保持布局平衡 */ +.nav-right { + display: flex; + align-items: center; + justify-content: flex-end; +} + +/* 内容区域样式强化 */ +.content-area { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + padding: 24rpx; + box-sizing: border-box; /* 确保padding不会增加元素总宽度 */ + /* 移除可能存在的margin-top,避免与padding-top冲突 */ + margin-top: 0; +} + +/* 当前版本卡片 */ +.current-version-card { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%); + color: white; + border-radius: var(--radius-large); + padding: 36rpx; + margin-bottom: 32rpx; + box-shadow: var(--shadow-medium); + animation: fadeIn 0.5s ease; +} + +.current-version-badge { + background: rgba(255, 255, 255, 0.2); + padding: 8rpx 16rpx; + border-radius: 16rpx; + font-size: 24rpx; + display: inline-block; + margin-bottom: 16rpx; +} + +.current-version-info { + display: flex; + align-items: baseline; + gap: 16rpx; +} + +.current-version-name { + font-size: 40rpx; + font-weight: 700; +} + +.current-version-date { + font-size: 28rpx; + opacity: 0.9; +} + +/* 日志列表 */ +.log-list { + display: flex; + flex-direction: column; + gap: 24rpx; +} + +/* 版本项 */ +.version-item { + background: var(--surface-color); + border-radius: var(--radius-medium); + padding: 32rpx; + box-shadow: var(--shadow-light); + border: 1rpx solid var(--border-color); + transition: all 0.3s ease; + animation: slideUp 0.4s ease; +} + +.version-item:active { + transform: scale(0.98); + box-shadow: var(--shadow-medium); +} + +/* 版本头部 */ +.version-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24rpx; + padding-bottom: 16rpx; + border-bottom: 1rpx solid var(--border-color); +} + +/* 版本名称 */ +.version-name { + font-size: 32rpx; + font-weight: 600; + color: var(--primary-color); +} + +/* 版本日期 */ +.version-date { + font-size: 24rpx; + color: var(--text-secondary); +} + +/* 版本内容 */ +.version-content { + display: flex; + flex-direction: column; + gap: 24rpx; +} + +/* 更新区域 */ +.update-section { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +/* 区域标题 */ +.section-title { + display: flex; + align-items: center; + gap: 8rpx; + margin-bottom: 8rpx; +} + +.section-icon { + font-size: 28rpx; +} + +.section-text { + font-size: 28rpx; + font-weight: 500; + color: var(--text-primary); +} + +/* 区域内容 */ +.section-items { + padding-left: 36rpx; +} + +/* 更新项 */ +.update-item { + font-size: 26rpx; + color: var(--text-secondary); + display: block; + margin-bottom: 16rpx; + line-height: 1.6; + position: relative; +} + +.update-item:last-child { + margin-bottom: 0; +} + +/* 加载更多 */ +.load-more { + text-align: center; + padding: 32rpx; + font-size: 28rpx; + color: var(--text-secondary); + display: flex; + align-items: center; + justify-content: center; + gap: 8rpx; + cursor: pointer; + transition: all 0.2s ease; +} + +.load-more:active { + color: var(--primary-color); + transform: scale(0.96); +} + +.load-more-icon { + font-size: 24rpx; +} + +/* 没有更多内容 */ +.no-more { + text-align: center; + padding: 48rpx 32rpx; + font-size: 26rpx; + color: var(--text-tertiary); +} + +/* 动画效果 */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10rpx); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20rpx); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 响应式设计 */ +@media screen and (max-width: 375px) { + .current-version-name { + font-size: 36rpx; + } + + .current-version-date { + font-size: 24rpx; + } + + .version-name { + font-size: 28rpx; + } + + .section-text { + font-size: 26rpx; + } + + .update-item { + font-size: 24rpx; + } +} + +/* 加载动画 */ +.loading { + display: inline-block; + width: 20rpx; + height: 20rpx; + border: 2rpx solid var(--border-color); + border-radius: 50%; + border-top-color: var(--primary-color); + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} diff --git a/pages/settings/account-security/account-security.js b/pages/settings/account-security/account-security.js new file mode 100644 index 0000000..7bbc544 --- /dev/null +++ b/pages/settings/account-security/account-security.js @@ -0,0 +1,115 @@ +// 账号与安全页面逻辑 +Page({ + /** + * 页面的初始数据 + */ + data: { + phone: '+86 18500006666', + wechatStatus: '已绑定', + email: 'annabk666@gmail.com', + // 初始化默认值,避免null导致的错误 + menuButtonInfo: { + height: 32, // 默认高度 + width: 32, // 默认宽度 + top: 0 + }, + statusBarHeight: 0 + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad: function(options) { + console.log('账号与安全页面加载'); + try { + // 获取系统信息,用于导航栏定位 + const systemInfo = wx.getSystemInfoSync(); + const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); + + this.setData({ + statusBarHeight: systemInfo.statusBarHeight, + menuButtonInfo: menuButtonInfo, + // 计算导航栏高度 = 胶囊按钮底部距离 - 状态栏高度 + navbarHeight: menuButtonInfo.bottom - systemInfo.statusBarHeight + }); + } catch (error) { + console.error('获取系统信息失败:', error); + // 如果获取失败,使用默认值 + this.setData({ + statusBarHeight: 20, + navbarHeight: 44 + }); + } + }, + + /** + * 返回上一页 + */ + navigateBack: function() { + wx.navigateBack({ + delta: 1 + }); + }, + + /** + * 查看手机号码 + */ + viewPhone: function() { + wx.showModal({ + title: '手机号码', + content: this.data.phone, + showCancel: false + }); + }, + + /** + * 查看微信绑定 + */ + viewWechat: function() { + wx.showModal({ + title: '微信绑定', + content: '您的账号已绑定微信', + showCancel: false + }); + }, + + /** + * 查看邮箱 + */ + viewEmail: function() { + wx.showModal({ + title: '邮箱', + content: this.data.email, + showCancel: false + }); + }, + + /** + * 推荐给好友 + */ + recommendToFriend: function() { + wx.showShareMenu({ + withShareTicket: true, + menus: ['shareAppMessage', 'shareTimeline'] + }); + }, + + /** + * 查看已屏蔽账户 + */ + viewBlockedAccounts: function() { + wx.navigateTo({ + url: '/pages/settings/account-security/blocked-accounts' + }); + }, + + /** + * 用户点击右上角分享 + */ + onShareAppMessage: function() { + return { + title: 'FindMe - 账号与安全', + path: '/pages/settings/account-security/account-security' + }; + } +}); \ No newline at end of file diff --git a/pages/settings/account-security/account-security.json b/pages/settings/account-security/account-security.json new file mode 100644 index 0000000..8ca571d --- /dev/null +++ b/pages/settings/account-security/account-security.json @@ -0,0 +1,8 @@ +{ + "navigationBarTitleText": "账户与安全", + "navigationBarBackgroundColor": "#000000", + "navigationBarTextStyle": "white", + "navigationStyle": "custom", + "backgroundColor": "#000000", + "disableScroll": false +} \ No newline at end of file diff --git a/pages/settings/account-security/account-security.wxml b/pages/settings/account-security/account-security.wxml new file mode 100644 index 0000000..014b81c --- /dev/null +++ b/pages/settings/account-security/account-security.wxml @@ -0,0 +1,63 @@ + + + + + + + + 账户与安全 + + + + + + + + 隐私 + + 📞 + + 电话 + {{phone}} + + + + + 📱 + + 微信 + {{wechatStatus}} + + + + + + + 邮箱 + {{email}} + + + + + + + + + 社交账户 + + 👥 + + 推荐给好友 + + + + + 🚫 + + 已屏蔽账户 + + + + + + \ No newline at end of file diff --git a/pages/settings/account-security/account-security.wxss b/pages/settings/account-security/account-security.wxss new file mode 100644 index 0000000..eeeab8c --- /dev/null +++ b/pages/settings/account-security/account-security.wxss @@ -0,0 +1,159 @@ +/* 账号与安全页面样式 - 深色主题 */ +.container { + width: 100%; + min-height: 100vh; + background-color: #000000; + color: #ffffff; +} + +/* 导航栏样式 - 移除固定高度设置,由JS动态控制 */ +.navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + justify-content: space-between; + background-color: #1a1a1a; + padding: 0 16px; + z-index: 10; + box-sizing: content-box; /* 确保padding不会影响总高度计算 */ +} + +.back-btn { + width: 48px; + display: flex; + align-items: center; + justify-content: center; +} + +.icon { + font-size: 20px; + color: #ffffff; +} + +.title { + flex: 1; + text-align: center; + font-size: 34rpx; + font-weight: 500; + color: #ffffff; +} + +/* 主内容区 */ +.content { + width: 100%; + box-sizing: border-box; + padding: 16px; + padding-top: calc(44px + env(safe-area-inset-top) + 16px); + min-height: 100vh; +} + +/* 分区样式 */ +.section { + margin-bottom: 24px; + background-color: #1a1a1a; + border-radius: 12px; + overflow: hidden; +} + +.section-title { + display: block; + padding: 16px; + font-size: 14px; + color: #888888; + background-color: #121212; +} + +/* 列表项样式 */ +.item { + display: flex; + align-items: center; + padding: 16px; + border-bottom: 1px solid #333333; +} + +.item:last-child { + border-bottom: none; +} + +.item-icon { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + margin-right: 16px; +} + +.item-content { + flex: 1; +} + +.item-title { + font-size: 16px; + color: #ffffff; + margin-bottom: 4px; + display: block; +} + +.item-value { + font-size: 14px; + color: #aaaaaa; +} + +.item-arrow { + font-size: 16px; + color: #888888; +} + +/* 开关样式 */ +.switch-container { + display: flex; + align-items: center; + justify-content: flex-end; + flex: 1; +} + +/* 图标颜色 */ +.phone { + color: #07c160; +} + +.wechat { + color: #07c160; +} + +.email { + color: #1aad19; +} + +.password { + color: #07c160; +} + +.twofactor { + color: #07c160; +} + +.recommend { + color: #07c160; +} + +.blocked { + color: #ff4d4f; +} + +/* 安全区域适配 */ +@media screen and (device-width: 375px) and (device-height: 812px) { + .navbar { + padding-top: 44px; + height: 88px; + } + + .content { + padding-top: 104px; + } +} \ No newline at end of file diff --git a/pages/settings/feedback/feedback.js b/pages/settings/feedback/feedback.js new file mode 100644 index 0000000..b05c23e --- /dev/null +++ b/pages/settings/feedback/feedback.js @@ -0,0 +1,109 @@ +// 反馈页面逻辑 +Page({ + /** + * 页面的初始数据 + */ + data: { + feedbackTypes: ['功能建议', 'Bug反馈', '内容纠错', '其他问题'], + selectedType: 0, + content: '', + contact: '', + navbarHeight: 0, // 导航栏高度 + statusBarHeight: 0, // 状态栏高度 + menuButtonInfo: {} // 胶囊按钮信息 + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad: function(options) { + console.log('反馈页面加载'); + + // 获取系统信息,用于导航栏定位 + const systemInfo = wx.getSystemInfoSync(); + const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); + + this.setData({ + statusBarHeight: systemInfo.statusBarHeight, + menuButtonInfo: menuButtonInfo, + // 计算导航栏高度 = 胶囊按钮底部距离 - 状态栏高度 + navbarHeight: menuButtonInfo.bottom - systemInfo.statusBarHeight + }); + }, + + /** + * 返回上一页 + */ + navigateBack: function() { + wx.navigateBack({ + delta: 1 + }); + }, + + /** + * 选择反馈类型 + */ + onTypeChange: function(e) { + this.setData({ + selectedType: e.detail.value + }); + }, + + /** + * 输入反馈内容 + */ + onContentChange: function(e) { + this.setData({ + content: e.detail.value + }); + }, + + /** + * 输入联系方式 + */ + onContactChange: function(e) { + this.setData({ + contact: e.detail.value + }); + }, + + /** + * 提交反馈 + */ + submitFeedback: function() { + const { selectedType, content, contact } = this.data; + const selectedTypeText = this.data.feedbackTypes[selectedType]; + + // 验证反馈内容 + if (!content.trim()) { + wx.showToast({ + title: '请输入反馈内容', + icon: 'none' + }); + return; + } + + // 显示加载提示 + wx.showLoading({ + title: '提交中...', + }); + + // 模拟提交反馈 + setTimeout(() => { + wx.hideLoading(); + + // 显示成功提示 + wx.showToast({ + title: '反馈提交成功', + icon: 'success', + duration: 2000, + success: () => { + // 延迟返回上一页 + setTimeout(() => { + this.navigateBack(); + }, 1500); + } + }); + }, 1000); + } +}); \ No newline at end of file diff --git a/pages/settings/feedback/feedback.json b/pages/settings/feedback/feedback.json new file mode 100644 index 0000000..aa325d0 --- /dev/null +++ b/pages/settings/feedback/feedback.json @@ -0,0 +1,8 @@ +{ + "navigationBarTitleText": "意见反馈", + "navigationBarBackgroundColor": "#000000", + "navigationBarTextStyle": "white", + "navigationStyle": "custom", + "backgroundColor": "#000000", + "disableScroll": false +} diff --git a/pages/settings/feedback/feedback.wxml b/pages/settings/feedback/feedback.wxml new file mode 100644 index 0000000..bd1972f --- /dev/null +++ b/pages/settings/feedback/feedback.wxml @@ -0,0 +1,56 @@ + + + + + + + + 意见反馈 + + + + + + + + + 反馈类型 + + + {{feedbackTypes[selectedType] || '请选择反馈类型'}} + + + + + + 反馈内容 + + + + + 联系方式 + + + + + + + 提交 + + + 提交中... + + + + + \ No newline at end of file diff --git a/pages/settings/feedback/feedback.wxss b/pages/settings/feedback/feedback.wxss new file mode 100644 index 0000000..bed6f00 --- /dev/null +++ b/pages/settings/feedback/feedback.wxss @@ -0,0 +1,169 @@ +/* 深色主题反馈页面样式 */ +.container { + width: 100%; + min-height: 100vh; + background: linear-gradient(to bottom, #23013a, #000000); + /* color: #ffffff; */ +} + +/* 导航栏样式 - 移除固定高度设置,由JS动态控制 */ +.navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + /* background-color: #1a1a1a; */ + /* border-bottom: 1px solid #333333; */ + z-index: 10; + box-sizing: content-box; /* 确保padding不会影响总高度计算 */ +} + +.back-btn { + width: 48px; + display: flex; + align-items: center; + justify-content: center; +} + +.icon { + font-size: 20px; + color: #ffffff; +} + +.title { + flex: 1; + text-align: center; + font-size: 34rpx; + font-weight: 500; + color: #ffffff; +} + +/* 主内容区 - 移除固定padding-top,由JS动态控制 */ +.content { + width: 100%; + box-sizing: border-box; +} + +/* 表单区域 */ +.form-section { + padding: 20px 16px; + background-color: #1a1a1a; + margin: 10px; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.form-item { + margin-bottom: 24px; +} + +.label { + display: block; + margin-bottom: 10px; + font-size: 15px; + color: #cccccc; + font-weight: 500; +} + +/* 选择器样式 */ +.picker-view { + width: 100%; + height: 48px; + line-height: 48px; + padding: 0 12px; + border: 1px solid #444444; + border-radius: 8px; + box-sizing: border-box; + color: #ffffff; + background-color: #2a2a2a; +} + +/* 输入框样式 */ +.textarea { + width: 100%; + min-height: 150px; + padding: 12px; + border: 1px solid #444444; + border-radius: 8px; + box-sizing: border-box; + font-size: 14px; + line-height: 1.6; + color: #ffffff; + background-color: #2a2a2a; + resize: none; +} + +.textarea::placeholder { + color: #888888; +} + +.input { + width: 100%; + height: 48px; + padding: 0 12px; + border: 1px solid #444444; + border-radius: 8px; + box-sizing: border-box; + font-size: 14px; + color: #ffffff; + background-color: #2a2a2a; +} + +.input::placeholder { + color: #888888; +} + +/* 提交按钮样式 */ +/* 提交按钮容器 */ +.submit-container { + display: flex; + justify-content: center; + padding: 0 30rpx; + margin-top: 60rpx; +} + +/* 提交按钮 - 渐变效果更贴近设计图 */ +.submit-button { + width: 100%; + height: 90rpx; + border-radius: 45rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + font-weight: 600; + transition: all 0.3s ease; +} + +.submit-text { + color: #ffffff; + font-weight: 600; +} + +/* 渐变效果 - 更接近设计图的粉蓝渐变 */ +.gradient { + background: linear-gradient(90deg, #FF55C9 0%, #00B8FF 100%); + background-size: 200% 200%; + animation: gradientFlow 3s ease infinite; +} + +@keyframes gradientFlow { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +/* 提交中状态 */ +.submitting { + background-color: #333333; + color: #999999; +} + +/* 触摸效果 */ +.submit-button:active:not(.submitting) { + transform: scale(0.98); + opacity: 0.9; +} + \ No newline at end of file diff --git a/pages/settings/notification-settings/notification-settings.js b/pages/settings/notification-settings/notification-settings.js new file mode 100644 index 0000000..f9c3632 --- /dev/null +++ b/pages/settings/notification-settings/notification-settings.js @@ -0,0 +1,592 @@ +// 🔔 通知设置页面逻辑 +const notificationManager = require('../../../utils/notification-manager.js'); + +Page({ + data: { + // 系统信息 + statusBarHeight: 44, + navBarHeight: 88, + + // 通知设置 + notificationSettings: { + enabled: true, + message: { + enabled: true, + showPreview: true, + sound: true, + vibrate: true, + groupByChat: true + }, + group: { + enabled: true, + mentionOnly: false, + sound: true, + vibrate: true + }, + friend: { + enabled: true, + sound: true + }, + system: { + enabled: true + } + }, + + // 免打扰设置 + doNotDisturb: { + enabled: false, + startTime: '22:00', + endTime: '08:00', + allowUrgent: true, + allowMentions: true + }, + + // 未读计数 + unreadCount: 0, + + // 时间选择器 + showTimePicker: false, + selectedTime: '22:00', + timePickerType: 'start', // start, end + + // 加载状态 + loading: false + }, + + onLoad(options) { + console.log('🔔 通知设置页面加载'); + + // 获取系统信息 + this.getSystemInfo(); + + // 加载通知设置 + this.loadNotificationSettings(); + + // 加载未读计数 + this.loadUnreadCount(); + }, + + onShow() { + console.log('🔔 通知设置页面显示'); + + // 刷新未读计数 + this.loadUnreadCount(); + }, + + // 获取系统信息 + getSystemInfo() { + try { + const windowInfo = wx.getWindowInfo(); + this.setData({ + statusBarHeight: windowInfo.statusBarHeight || 44, + navBarHeight: 88 + }); + } catch (error) { + console.error('获取系统信息失败:', error); + this.setData({ + statusBarHeight: 44, + navBarHeight: 88 + }); + } + }, + + // 加载通知设置 + loadNotificationSettings() { + try { + // 从通知管理器获取设置 + const settings = notificationManager.getStatus(); + + if (settings.notificationSettings) { + this.setData({ + notificationSettings: { + ...this.data.notificationSettings, + ...settings.notificationSettings + } + }); + } + + // 加载免打扰设置 + const doNotDisturbSettings = wx.getStorageSync('doNotDisturbSettings'); + if (doNotDisturbSettings) { + this.setData({ + doNotDisturb: { + ...this.data.doNotDisturb, + ...doNotDisturbSettings + } + }); + } + + console.log('✅ 通知设置加载完成'); + + } catch (error) { + console.error('❌ 加载通知设置失败:', error); + } + }, + + // 加载未读计数 + loadUnreadCount() { + try { + const totalUnread = notificationManager.getTotalUnreadCount(); + this.setData({ + unreadCount: totalUnread + }); + } catch (error) { + console.error('❌ 加载未读计数失败:', error); + } + }, + + // 🔔 ===== 通知设置变更 ===== + + // 通知总开关变更 + onNotificationEnabledChange(e) { + const enabled = e.detail.value; + console.log('🔔 通知总开关变更:', enabled); + + this.setData({ + 'notificationSettings.enabled': enabled + }); + + this.saveNotificationSettings(); + }, + + // 消息通知变更 + onMessageNotificationChange(e) { + const enabled = e.detail.value; + console.log('💬 消息通知变更:', enabled); + + this.setData({ + 'notificationSettings.message.enabled': enabled + }); + + this.saveNotificationSettings(); + }, + + // 消息预览变更 + onMessagePreviewChange(e) { + const enabled = e.detail.value; + console.log('👁️ 消息预览变更:', enabled); + + this.setData({ + 'notificationSettings.message.showPreview': enabled + }); + + this.saveNotificationSettings(); + }, + + // 消息提示音变更 + onMessageSoundChange(e) { + const enabled = e.detail.value; + console.log('🔊 消息提示音变更:', enabled); + + this.setData({ + 'notificationSettings.message.sound': enabled + }); + + this.saveNotificationSettings(); + }, + + // 消息震动变更 + onMessageVibrateChange(e) { + const enabled = e.detail.value; + console.log('📳 消息震动变更:', enabled); + + this.setData({ + 'notificationSettings.message.vibrate': enabled + }); + + this.saveNotificationSettings(); + }, + + // 消息分组变更 + onMessageGroupChange(e) { + const enabled = e.detail.value; + console.log('📂 消息分组变更:', enabled); + + this.setData({ + 'notificationSettings.message.groupByChat': enabled + }); + + this.saveNotificationSettings(); + }, + + // 群聊通知变更 + onGroupNotificationChange(e) { + const enabled = e.detail.value; + console.log('👥 群聊通知变更:', enabled); + + this.setData({ + 'notificationSettings.group.enabled': enabled + }); + + this.saveNotificationSettings(); + }, + + // 群聊仅@提醒变更 + onGroupMentionOnlyChange(e) { + const enabled = e.detail.value; + console.log('@ 群聊仅@提醒变更:', enabled); + + this.setData({ + 'notificationSettings.group.mentionOnly': enabled + }); + + this.saveNotificationSettings(); + }, + + // 群聊提示音变更 + onGroupSoundChange(e) { + const enabled = e.detail.value; + console.log('🔊 群聊提示音变更:', enabled); + + this.setData({ + 'notificationSettings.group.sound': enabled + }); + + this.saveNotificationSettings(); + }, + + // 群聊震动变更 + onGroupVibrateChange(e) { + const enabled = e.detail.value; + console.log('📳 群聊震动变更:', enabled); + + this.setData({ + 'notificationSettings.group.vibrate': enabled + }); + + this.saveNotificationSettings(); + }, + + // 好友通知变更 + onFriendNotificationChange(e) { + const enabled = e.detail.value; + console.log('👤 好友通知变更:', enabled); + + this.setData({ + 'notificationSettings.friend.enabled': enabled + }); + + this.saveNotificationSettings(); + }, + + // 好友提示音变更 + onFriendSoundChange(e) { + const enabled = e.detail.value; + console.log('🔊 好友提示音变更:', enabled); + + this.setData({ + 'notificationSettings.friend.sound': enabled + }); + + this.saveNotificationSettings(); + }, + + // 系统通知变更 + onSystemNotificationChange(e) { + const enabled = e.detail.value; + console.log('⚙️ 系统通知变更:', enabled); + + this.setData({ + 'notificationSettings.system.enabled': enabled + }); + + this.saveNotificationSettings(); + }, + + // 🔕 ===== 免打扰设置 ===== + + // 免打扰模式变更 + onDoNotDisturbChange(e) { + const enabled = e.detail.value; + console.log('🔕 免打扰模式变更:', enabled); + + this.setData({ + 'doNotDisturb.enabled': enabled + }); + + this.saveDoNotDisturbSettings(); + }, + + // 选择开始时间 + selectStartTime() { + console.log('⏰ 选择开始时间'); + + this.setData({ + showTimePicker: true, + selectedTime: this.data.doNotDisturb.startTime, + timePickerType: 'start' + }); + }, + + // 选择结束时间 + selectEndTime() { + console.log('⏰ 选择结束时间'); + + this.setData({ + showTimePicker: true, + selectedTime: this.data.doNotDisturb.endTime, + timePickerType: 'end' + }); + }, + + // 时间选择变更 + onTimeChange(e) { + const time = e.detail.value; + console.log('⏰ 时间选择变更:', time, this.data.timePickerType); + + if (this.data.timePickerType === 'start') { + this.setData({ + 'doNotDisturb.startTime': time, + showTimePicker: false + }); + } else { + this.setData({ + 'doNotDisturb.endTime': time, + showTimePicker: false + }); + } + + this.saveDoNotDisturbSettings(); + }, + + // 取消时间选择 + onTimeCancel() { + this.setData({ + showTimePicker: false + }); + }, + + // 允许紧急通知变更 + onAllowUrgentChange(e) { + const enabled = e.detail.value; + console.log('🚨 允许紧急通知变更:', enabled); + + this.setData({ + 'doNotDisturb.allowUrgent': enabled + }); + + this.saveDoNotDisturbSettings(); + }, + + // 允许@提醒变更 + onAllowMentionsChange(e) { + const enabled = e.detail.value; + console.log('@ 允许@提醒变更:', enabled); + + this.setData({ + 'doNotDisturb.allowMentions': enabled + }); + + this.saveDoNotDisturbSettings(); + }, + + // 📋 ===== 通知管理 ===== + + // 查看通知历史 + viewNotificationHistory() { + console.log('📋 查看通知历史'); + + wx.navigateTo({ + url: '/pages/settings/notification-history/notification-history' + }); + }, + + // 清空通知历史 + clearNotificationHistory() { + console.log('🗑️ 清空通知历史'); + + wx.showModal({ + title: '清空通知历史', + content: '确定要删除所有通知记录吗?此操作不可恢复。', + confirmText: '清空', + confirmColor: '#FF3B30', + success: (res) => { + if (res.confirm) { + this.performClearHistory(); + } + } + }); + }, + + // 执行清空历史 + performClearHistory() { + try { + // 清空通知历史 + wx.removeStorageSync('notificationHistory'); + + // 重置未读计数 + notificationManager.reset(); + + this.setData({ + unreadCount: 0 + }); + + wx.showToast({ + title: '历史已清空', + icon: 'success' + }); + + console.log('✅ 通知历史清空完成'); + + } catch (error) { + console.error('❌ 清空通知历史失败:', error); + wx.showToast({ + title: '清空失败', + icon: 'none' + }); + } + }, + + // 📱 ===== 订阅消息 ===== + + // 请求订阅消息权限 + async requestSubscribeMessage() { + console.log('📝 请求订阅消息权限'); + + try { + const templateIds = [ + 'template_id_1', // 新消息通知 + 'template_id_2', // 好友请求通知 + 'template_id_3' // 系统通知 + ]; + + const result = await notificationManager.requestSubscribeMessage(templateIds); + + if (result) { + wx.showToast({ + title: '权限设置完成', + icon: 'success' + }); + } else { + wx.showToast({ + title: '权限设置失败', + icon: 'none' + }); + } + + } catch (error) { + console.error('❌ 请求订阅消息权限失败:', error); + wx.showToast({ + title: '权限设置失败', + icon: 'none' + }); + } + }, + + // 查看订阅状态 + viewSubscribeStatus() { + console.log('📊 查看订阅状态'); + + wx.showToast({ + title: '功能开发中', + icon: 'none' + }); + }, + + // ⚙️ ===== 设置管理 ===== + + // 保存通知设置 + saveNotificationSettings() { + try { + // 更新通知管理器设置 + notificationManager.updateNotificationSettings(this.data.notificationSettings); + + console.log('✅ 通知设置保存成功'); + + } catch (error) { + console.error('❌ 保存通知设置失败:', error); + } + }, + + // 保存免打扰设置 + saveDoNotDisturbSettings() { + try { + wx.setStorageSync('doNotDisturbSettings', this.data.doNotDisturb); + console.log('✅ 免打扰设置保存成功'); + + } catch (error) { + console.error('❌ 保存免打扰设置失败:', error); + } + }, + + // 重置设置 + resetSettings() { + console.log('🔄 重置设置'); + + wx.showModal({ + title: '重置通知设置', + content: '确定要将所有通知设置恢复为默认值吗?', + confirmText: '重置', + confirmColor: '#FF3B30', + success: (res) => { + if (res.confirm) { + this.performResetSettings(); + } + } + }); + }, + + // 执行重置设置 + performResetSettings() { + try { + // 重置为默认设置 + this.setData({ + notificationSettings: { + enabled: true, + message: { + enabled: true, + showPreview: true, + sound: true, + vibrate: true, + groupByChat: true + }, + group: { + enabled: true, + mentionOnly: false, + sound: true, + vibrate: true + }, + friend: { + enabled: true, + sound: true + }, + system: { + enabled: true + } + }, + doNotDisturb: { + enabled: false, + startTime: '22:00', + endTime: '08:00', + allowUrgent: true, + allowMentions: true + } + }); + + // 保存设置 + this.saveNotificationSettings(); + this.saveDoNotDisturbSettings(); + + wx.showToast({ + title: '设置已重置', + icon: 'success' + }); + + console.log('✅ 通知设置重置完成'); + + } catch (error) { + console.error('❌ 重置通知设置失败:', error); + wx.showToast({ + title: '重置失败', + icon: 'none' + }); + } + }, + + // 🧭 ===== 页面导航 ===== + + // 返回上一页 + goBack() { + wx.navigateBack(); + } +}); diff --git a/pages/settings/notification-settings/notification-settings.json b/pages/settings/notification-settings/notification-settings.json new file mode 100644 index 0000000..5c144e6 --- /dev/null +++ b/pages/settings/notification-settings/notification-settings.json @@ -0,0 +1,7 @@ +{ + "navigationStyle": "custom", + "backgroundColor": "#F2F2F7", + "backgroundTextStyle": "dark", + "enablePullDownRefresh": false, + "onReachBottomDistance": 50 +} diff --git a/pages/settings/notification-settings/notification-settings.wxml b/pages/settings/notification-settings/notification-settings.wxml new file mode 100644 index 0000000..d7bb930 --- /dev/null +++ b/pages/settings/notification-settings/notification-settings.wxml @@ -0,0 +1,312 @@ + + + + + + + + + + + 通知设置 + + + + + + + + + + + + 通知总开关 + + + + + 接收通知 + 关闭后将不会收到任何通知 + + + + + + + + + 消息通知 + + + + + 新消息通知 + 收到新消息时显示通知 + + + + + + + 消息预览 + 在通知中显示消息内容 + + + + + + + 消息提示音 + 收到消息时播放提示音 + + + + + + + 消息震动 + 收到消息时震动提醒 + + + + + + + 按聊天分组 + 将同一聊天的消息合并显示 + + + + + + + + + 群聊通知 + + + + + 群聊消息 + 收到群聊消息时显示通知 + + + + + + + 仅@我的消息 + 只有@我的群聊消息才通知 + + + + + + + 群聊提示音 + 收到群聊消息时播放提示音 + + + + + + + 群聊震动 + 收到群聊消息时震动提醒 + + + + + + + + + 好友通知 + + + + + 好友请求 + 收到好友请求时显示通知 + + + + + + + 好友提示音 + 收到好友通知时播放提示音 + + + + + + + + + 系统通知 + + + + + 系统消息 + 接收系统通知和公告 + + + + + + + + + 免打扰模式 + + + + + 开启免打扰 + 在指定时间段内不接收通知 + + + + + + + 开始时间 + 免打扰开始时间 + + + {{doNotDisturb.startTime}} + + + + + + + 结束时间 + 免打扰结束时间 + + + {{doNotDisturb.endTime}} + + + + + + + 允许紧急通知 + 免打扰期间仍接收紧急通知 + + + + + + + 允许@提醒 + 免打扰期间仍接收@提醒 + + + + + + + + + 通知管理 + + + + + 通知历史 + 查看最近的通知记录 + + + {{unreadCount}} + + + + + + + 清空通知历史 + 删除所有通知记录 + + + + + + + + + 订阅消息 + + + + + 订阅消息权限 + 允许应用发送订阅消息 + + + + + + + 订阅状态 + 查看当前订阅消息状态 + + + + + + + + + + 重置通知设置 + 恢复所有通知设置为默认值 + + + + + + + + + + diff --git a/pages/settings/notification-settings/notification-settings.wxss b/pages/settings/notification-settings/notification-settings.wxss new file mode 100644 index 0000000..255a276 --- /dev/null +++ b/pages/settings/notification-settings/notification-settings.wxss @@ -0,0 +1,582 @@ +/* 🔔 通知设置页面样式 */ + +/* CSS变量定义 */ +page { + --primary-color: #007AFF; + --primary-light: #5AC8FA; + --primary-dark: #0051D5; + --success-color: #34C759; + --danger-color: #FF3B30; + --warning-color: #FF9500; + --background-color: #F2F2F7; + --surface-color: #FFFFFF; + --text-primary: #000000; + --text-secondary: #8E8E93; + --text-tertiary: #C7C7CC; + --border-color: #E5E5EA; + --shadow-light: 0 1rpx 3rpx rgba(0, 0, 0, 0.1); + --shadow-medium: 0 4rpx 12rpx rgba(0, 0, 0, 0.15); + --radius-small: 8rpx; + --radius-medium: 12rpx; + --radius-large: 20rpx; +} + +/* 🌙 深色模式支持 */ +@media (prefers-color-scheme: dark) { + page { + --primary-color: #0A84FF; + --primary-light: #64D2FF; + --primary-dark: #0056CC; + --success-color: #30D158; + --danger-color: #FF453A; + --warning-color: #FF9F0A; + --background-color: #000000; + --surface-color: #1C1C1E; + --text-primary: #FFFFFF; + --text-secondary: #8E8E93; + --text-tertiary: #48484A; + --border-color: #38383A; + --shadow-light: 0 1rpx 3rpx rgba(0, 0, 0, 0.3); + --shadow-medium: 0 4rpx 12rpx rgba(0, 0, 0, 0.4); + } +} + +.notification-settings-container { + height: 100vh; + background: var(--background-color); + display: flex; + flex-direction: column; +} + +/* 🎨 自定义导航栏 */ +.custom-navbar { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%); + box-shadow: var(--shadow-medium); + z-index: 1000; +} + +.navbar-content { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32rpx; +} + +.navbar-left, .navbar-right { + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-medium); + transition: all 0.3s ease; +} + +.navbar-left:active { + background: rgba(255, 255, 255, 0.2); + transform: scale(0.95); +} + +.back-icon { + font-size: 48rpx; + color: white; + font-weight: 300; +} + +.navbar-title { + flex: 1; + text-align: center; +} + +.title-text { + font-size: 36rpx; + font-weight: 600; + color: white; +} + +/* 🎨 页面内容 */ +.page-content { + flex: 1; + padding: 32rpx 0; +} + +/* 🎨 设置分组 */ +.settings-section { + margin-bottom: 32rpx; +} + +.section-header { + padding: 0 32rpx 16rpx; +} + +.section-title { + font-size: 26rpx; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 1rpx; +} + +/* 🎨 设置项 */ +.setting-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32rpx; + background: var(--surface-color); + border-bottom: 1rpx solid var(--border-color); + transition: all 0.2s ease; +} + +.setting-item:first-child { + border-top-left-radius: var(--radius-medium); + border-top-right-radius: var(--radius-medium); +} + +.setting-item:last-child { + border-bottom: none; + border-bottom-left-radius: var(--radius-medium); + border-bottom-right-radius: var(--radius-medium); +} + +.setting-item:active { + background: var(--background-color); +} + +.setting-item.danger { + background: rgba(255, 59, 48, 0.05); +} + +.setting-item.danger:active { + background: rgba(255, 59, 48, 0.1); +} + +.item-info { + flex: 1; + min-width: 0; +} + +.item-title { + font-size: 32rpx; + font-weight: 500; + color: var(--text-primary); + display: block; + margin-bottom: 8rpx; +} + +.item-title.danger { + color: var(--danger-color); +} + +.item-desc { + font-size: 26rpx; + color: var(--text-secondary); + line-height: 1.4; +} + +.item-action { + display: flex; + align-items: center; + gap: 16rpx; +} + +.action-text { + font-size: 30rpx; + color: var(--primary-color); + font-weight: 500; +} + +.action-arrow { + font-size: 32rpx; + color: var(--text-tertiary); + font-weight: 300; +} + +.unread-badge { + min-width: 40rpx; + height: 40rpx; + border-radius: 20rpx; + background: var(--danger-color); + color: white; + font-size: 24rpx; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + padding: 0 8rpx; +} + +/* 🎨 开关控件 */ +.setting-switch { + transform: scale(0.8); + transition: all 0.3s ease; +} + +.setting-switch:active { + transform: scale(0.75); +} + +/* 🎨 时间选择器样式 */ +.time-picker-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: flex-end; + animation: fadeIn 0.3s ease-out; +} + +.time-picker-content { + width: 100%; + background: var(--surface-color); + border-radius: var(--radius-large) var(--radius-large) 0 0; + box-shadow: var(--shadow-medium); + animation: slideUp 0.3s ease-out; + overflow: hidden; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +.time-picker-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32rpx; + border-bottom: 1rpx solid var(--border-color); +} + +.time-picker-title { + font-size: 36rpx; + font-weight: 600; + color: var(--text-primary); +} + +.time-picker-btn { + font-size: 30rpx; + color: var(--primary-color); + font-weight: 500; + transition: all 0.2s ease; +} + +.time-picker-btn:active { + opacity: 0.7; +} + +.time-picker-btn.cancel { + color: var(--text-secondary); +} + +.time-picker-body { + padding: 32rpx; +} + +/* 🎨 状态指示器 */ +.status-indicator { + display: flex; + align-items: center; + gap: 16rpx; +} + +.status-dot { + width: 16rpx; + height: 16rpx; + border-radius: 8rpx; + background: var(--text-tertiary); +} + +.status-dot.active { + background: var(--success-color); +} + +.status-dot.inactive { + background: var(--text-tertiary); +} + +.status-dot.error { + background: var(--danger-color); +} + +.status-text { + font-size: 26rpx; + color: var(--text-secondary); +} + +/* 🎨 通知预览 */ +.notification-preview { + background: var(--surface-color); + border: 1rpx solid var(--border-color); + border-radius: var(--radius-medium); + padding: 24rpx; + margin: 24rpx 32rpx; + box-shadow: var(--shadow-light); +} + +.preview-header { + display: flex; + align-items: center; + gap: 16rpx; + margin-bottom: 16rpx; +} + +.preview-icon { + width: 48rpx; + height: 48rpx; + border-radius: 24rpx; + background: var(--primary-color); + display: flex; + align-items: center; + justify-content: center; + font-size: 24rpx; + color: white; +} + +.preview-title { + font-size: 28rpx; + font-weight: 600; + color: var(--text-primary); +} + +.preview-time { + font-size: 24rpx; + color: var(--text-tertiary); + margin-left: auto; +} + +.preview-content { + font-size: 26rpx; + color: var(--text-secondary); + line-height: 1.4; +} + +/* 🎨 提示信息 */ +.tip-container { + background: rgba(0, 122, 255, 0.1); + border: 1rpx solid rgba(0, 122, 255, 0.3); + border-radius: var(--radius-small); + padding: 24rpx; + margin: 24rpx 32rpx; +} + +.tip-text { + font-size: 26rpx; + color: var(--primary-color); + line-height: 1.4; +} + +.tip-icon { + font-size: 28rpx; + margin-right: 12rpx; +} + +/* 🎨 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 120rpx 40rpx; + text-align: center; +} + +.empty-icon { + font-size: 120rpx; + margin-bottom: 24rpx; + opacity: 0.5; +} + +.empty-text { + font-size: 28rpx; + color: var(--text-secondary); + margin-bottom: 32rpx; +} + +.empty-action { + padding: 24rpx 48rpx; + background: var(--primary-color); + color: white; + border-radius: var(--radius-medium); + font-size: 28rpx; + font-weight: 500; + transition: all 0.3s ease; +} + +.empty-action:active { + background: var(--primary-dark); + transform: scale(0.98); +} + +/* 🎨 加载状态 */ +.loading-container { + display: flex; + align-items: center; + justify-content: center; + padding: 80rpx; +} + +.loading-spinner { + width: 60rpx; + height: 60rpx; + border: 4rpx solid var(--border-color); + border-top: 4rpx solid var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-text { + font-size: 28rpx; + color: var(--text-secondary); + margin-left: 24rpx; +} + +/* 📱 响应式设计 */ +@media screen and (max-width: 375px) { + .page-content { + padding: 24rpx 0; + } + + .settings-section { + margin-bottom: 24rpx; + } + + .section-header { + padding: 0 24rpx 12rpx; + } + + .setting-item { + padding: 24rpx; + } + + .notification-preview, + .tip-container { + margin: 16rpx 24rpx; + padding: 16rpx; + } +} + +@media screen and (min-width: 414px) { + .page-content { + padding: 40rpx 0; + } + + .settings-section { + margin-bottom: 40rpx; + } + + .section-header { + padding: 0 40rpx 20rpx; + } + + .setting-item { + padding: 40rpx; + } + + .notification-preview, + .tip-container { + margin: 32rpx 40rpx; + padding: 32rpx; + } +} + +/* 🎨 动画效果 */ +.setting-item { + transition: all 0.2s ease; +} + +.setting-item:hover { + background: var(--background-color); +} + +.setting-switch { + transition: all 0.3s ease; +} + +.action-text { + transition: all 0.2s ease; +} + +.action-text:active { + opacity: 0.7; +} + +/* 🎨 特殊状态 */ +.setting-item.disabled { + opacity: 0.5; + pointer-events: none; +} + +.setting-item.highlighted { + background: rgba(0, 122, 255, 0.05); + border-color: rgba(0, 122, 255, 0.3); +} + +.setting-item.warning { + background: rgba(255, 149, 0, 0.05); +} + +.setting-item.warning .item-title { + color: var(--warning-color); +} + +/* 🎨 分隔线 */ +.divider { + height: 1rpx; + background: var(--border-color); + margin: 0 32rpx; +} + +.divider.thick { + height: 16rpx; + background: var(--background-color); + margin: 0; +} + +/* 🎨 标签 */ +.tag { + display: inline-block; + padding: 8rpx 16rpx; + border-radius: var(--radius-small); + font-size: 24rpx; + font-weight: 500; + line-height: 1; +} + +.tag.primary { + background: rgba(0, 122, 255, 0.1); + color: var(--primary-color); +} + +.tag.success { + background: rgba(52, 199, 89, 0.1); + color: var(--success-color); +} + +.tag.warning { + background: rgba(255, 149, 0, 0.1); + color: var(--warning-color); +} + +.tag.danger { + background: rgba(255, 59, 48, 0.1); + color: var(--danger-color); +} diff --git a/pages/settingss/settingss.js b/pages/settingss/settingss.js new file mode 100644 index 0000000..deb63c6 --- /dev/null +++ b/pages/settingss/settingss.js @@ -0,0 +1,266 @@ +// pages/settingss/settingss.js +const app = getApp(); + +Page({ + data: { + currentTheme: '浅色', + notificationStatus: '已开启', + currentLanguage: '中文', + isDarkMode: false, + // 初始化默认值,避免null导致的错误 + menuButtonInfo: { + height: 32, // 默认高度 + width: 32, // 默认宽度 + top: 0 + }, + statusBarHeight: 0, + // 聊天设置数据 + chatSettings: { + fontSize: 'medium', + backgroundName: '默认背景', + showPreview: true + } + }, + + onLoad() { + try { + // 1. 获取系统信息(含状态栏高度) + const systemInfo = wx.getSystemInfoSync(); + // 2. 获取胶囊按钮位置信息(用于导航栏对齐) + const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); + + // 初始化全局设置对象 + if (!app.globalData.settings) { + app.globalData.settings = {}; + } + + // 更新数据 + this.setData({ + isDarkMode: systemInfo.theme === 'dark', + statusBarHeight: systemInfo.statusBarHeight, + menuButtonInfo: menuButtonInfo || this.data.menuButtonInfo, + currentTheme: app.globalData.settings.theme || '浅色', + notificationStatus: app.globalData.settings.notification || '已开启', + currentLanguage: app.globalData.settings.language || '中文' + }); + } catch (e) { + console.error('获取系统信息失败:', e); + } + }, + + // 返回上一页 + navigateBack() { + wx.navigateBack(); + }, + + // 切换主题 + toggleTheme(e) { + const isDark = e.detail.value; + const newTheme = isDark ? '深色' : '浅色'; + + this.setData({ + currentTheme: newTheme, + isDarkMode: isDark + }); + + app.globalData.settings.theme = newTheme; + + wx.showToast({ + title: `已切换到${newTheme}主题`, + icon: 'success' + }); + }, + + // 跳转到账号安全 + openAccountSecurity() { + wx.navigateTo({ + url: '/pages/settings/account-security/account-security' + }); + }, + + // 跳转到隐私设置 + openPrivacySettings() { + wx.navigateTo({ + url: '/pages/settings/privacy/privacy' + }); + }, + + // 主题设置 + openThemeSettings() { + const themes = ['浅色', '深色', '自动']; + wx.showActionSheet({ + itemList: themes, + success: (res) => { + const selectedTheme = themes[res.tapIndex]; + this.setData({ currentTheme: selectedTheme }); + app.globalData.settings.theme = selectedTheme; + wx.showToast({ + title: `已切换到${selectedTheme}主题`, + icon: 'success' + }); + } + }); + }, + + // 通知设置 + openNotificationSettings() { + const options = ['已开启', '已关闭', '勿扰模式']; + wx.showActionSheet({ + itemList: options, + success: (res) => { + const selectedStatus = options[res.tapIndex]; + this.setData({ notificationStatus: selectedStatus }); + app.globalData.settings.notification = selectedStatus; + wx.showToast({ + title: `通知已${selectedStatus === '已开启' ? '开启' : selectedStatus === '已关闭' ? '关闭' : '设为勿扰'}`, + icon: 'success' + }); + } + }); + }, + + // 语言设置 + openLanguageSettings() { + const languages = ['中文', 'English']; + wx.showActionSheet({ + itemList: languages, + success: (res) => { + const selectedLanguage = languages[res.tapIndex]; + this.setData({ currentLanguage: selectedLanguage }); + app.globalData.settings.language = selectedLanguage; + wx.showToast({ + title: `已切换到${selectedLanguage}`, + icon: 'success' + }); + } + }); + }, + + // 聊天设置 + openChatSettings() { + const options = ['字体大小', '聊天背景', '信息预览']; + wx.showActionSheet({ + itemList: options, + success: (res) => { + switch(res.tapIndex) { + case 0: // 字体大小 + this.selectFontSize(); + break; + case 1: // 聊天背景 + this.selectChatBackground(); + break; + case 2: // 信息预览 + this.toggleMessagePreview(); + break; + } + } + }); + }, + + // 选择字体大小 + selectFontSize() { + const fontSizeOptions = ['小', '中', '大']; + wx.showActionSheet({ + itemList: fontSizeOptions, + success: (res) => { + const fontSize = ['small', 'medium', 'large'][res.tapIndex]; + const chatSettings = this.data.chatSettings; + chatSettings.fontSize = fontSize; + + this.setData({ + chatSettings: chatSettings + }); + + // 保存设置 + wx.setStorageSync('chatSettings', this.data.chatSettings); + + wx.showToast({ + title: `字体大小已设为${fontSizeOptions[res.tapIndex]}`, + icon: 'success' + }); + } + }); + }, + + // 选择聊天背景 + selectChatBackground() { + const options = ['默认背景', '渐变蓝', '渐变紫', '自然风光', '抽象艺术']; + wx.showActionSheet({ + itemList: options, + success: (res) => { + const backgroundName = options[res.tapIndex]; + const chatSettings = this.data.chatSettings; + chatSettings.backgroundName = backgroundName; + + this.setData({ + chatSettings: chatSettings + }); + + // 保存设置 + wx.setStorageSync('chatSettings', this.data.chatSettings); + + wx.showToast({ + title: `已切换到${backgroundName}`, + icon: 'success' + }); + } + }); + }, + + // 切换消息预览 + toggleMessagePreview() { + const showPreview = !this.data.chatSettings.showPreview; + const chatSettings = this.data.chatSettings; + chatSettings.showPreview = showPreview; + + this.setData({ + chatSettings: chatSettings + }); + + // 保存设置 + wx.setStorageSync('chatSettings', this.data.chatSettings); + + wx.showToast({ + title: `消息预览已${showPreview ? '开启' : '关闭'}`, + icon: 'success' + }); + }, + + // 意见反馈 + openFeedback() { + wx.navigateTo({ + url: '/pages/settings/feedback/feedback' + }); + }, + + // 关于 + openAbout() { + wx.navigateTo({ + url: '/pages/settings/about/about' + }); + }, + + // 退出登录 + logout() { + wx.showModal({ + title: '退出登录', + content: '确定要退出登录吗?', + success: (res) => { + if (res.confirm) { + // 模拟退出登录 + wx.showLoading({ + title: '退出中...', + }); + + // 模拟网络请求延迟 + setTimeout(() => { + wx.hideLoading(); + wx.reLaunch({ + url: '/pages/login/login' + }); + }, 1000); + } + } + }); + } +}); \ No newline at end of file diff --git a/pages/settingss/settingss.json b/pages/settingss/settingss.json new file mode 100644 index 0000000..c50fb55 --- /dev/null +++ b/pages/settingss/settingss.json @@ -0,0 +1,9 @@ +{ + "navigationBarTitleText": "设置", + "navigationBarBackgroundColor": "#000000", + "navigationBarTextStyle": "white", + "backgroundColor": "#000000", + "disableScroll": false, + "navigationStyle": "custom", + "pageOrientation": "portrait" +} \ No newline at end of file diff --git a/pages/settingss/settingss.wxml b/pages/settingss/settingss.wxml new file mode 100644 index 0000000..116206c --- /dev/null +++ b/pages/settingss/settingss.wxml @@ -0,0 +1,136 @@ + + + + + + + + 设置 + + + + + + + + + + 账户与安全 + + + + 🛡️ + + 账号与安全 + 手机号、邮箱管理 + + + 安全 + + + + + + 👁️ + + 隐私设置 + 朋友权限、位置服务 + + + + + + + + + + 个性化 + + + + 🌙 + + 主题设置 + 深色模式、主题颜色 + + + {{currentTheme || '浅色'}} + + + + + + 🔔 + + 消息通知 + 声音、震动、勿扰模式 + + + {{notificationStatus || '已开启'}} + + + + + + 🌐 + + 语言设置 + 界面语言 + + + {{currentLanguage || '中文'}} + + + + + + 💬 + + 聊天设置 + 聊天背景、字体大小 + + + + + + + + + + 帮助与反馈 + + + + + + 意见反馈 + 提交故障、建议,及联系客服 + + + + + + ℹ️ + + 关于我们 + 版本信息、使用条款 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pages/settingss/settingss.wxss b/pages/settingss/settingss.wxss new file mode 100644 index 0000000..96289d9 --- /dev/null +++ b/pages/settingss/settingss.wxss @@ -0,0 +1,190 @@ +/* 修复标题位置的关键样式 */ +/* 页面容器 */ +.settings-container { + width: 100%; + height: 100vh; + background: #000000; + display: flex; + flex-direction: column; + /* color: #ffffff; */ +} + +/* 导航栏样式 - 移除固定高度设置,由JS动态控制 */ +.navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + /* background-color: #1a1a1a; */ + /* border-bottom: 1px solid #333333; */ + z-index: 10; + box-sizing: content-box; /* 确保padding不会影响总高度计算 */ +} + +.back-btn { + width: 48px; + display: flex; + align-items: center; + justify-content: center; +} + +.icon { + font-size: 20px; + color: #ffffff; +} + +.title { + flex: 1; + text-align: center; + font-size: 34rpx; + font-weight: 500; + color: #ffffff; +} + + + +/* 滚动内容区 */ +.settings-content { + flex: 1; + width: 100%; + box-sizing: border-box; + padding-left: 30rpx; + padding-right: 30rpx; + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} + +/* 菜单组样式 */ +.menu-group { + background-color: #1a1a1a; /* 深色卡片背景,与黑色区分 */ + border-radius: 20rpx; + margin-bottom: 30rpx; + overflow: hidden; +} + +/* 组标题 */ +.group-header { + display: flex; + align-items: center; + padding: 30rpx 30rpx 20rpx; +} +.group-icon { + font-size: 36rpx; + margin-right: 20rpx; + color: #ffffff; +} +.group-title { + color: #ffffff; + font-size: 30rpx; + font-weight: 500; +} + +/* 菜单项 */ +.menu-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 25rpx 30rpx; + border-top: 1px solid #333333; /* 浅色边框,区分菜单项 */ +} +.menu-item:first-child { + border-top: none; /* 第一个菜单项去掉上边框 */ +} + +/* 菜单图标 */ +.menu-icon { + font-size: 36rpx; + margin-right: 25rpx; + width: 40rpx; /* 固定宽度,避免图标大小不一导致错位 */ + text-align: center; +} +.security { color: #4CAF50; } +.privacy { color: #2196F3; } +.theme { color: #FFC107; } +.notification { color: #FF9800; } +.language { color: #9C27B0; } +.chat { color: #E91E63; } +.feedback { color: #00BCD4; } +.about { color: #673AB7; } + +/* 菜单内容 */ +.menu-content { + flex: 1; /* 占满剩余空间 */ + min-width: 0; /* 解决文字过长溢出问题 */ +} +.menu-title { + color: #ffffff; + font-size: 30rpx; + display: block; /* 标题单独一行 */ + margin-bottom: 5rpx; +} +.menu-subtitle { + color: #aaaaaa; /* 浅色副标题 */ + font-size: 24rpx; +} + +/* 菜单状态文本 */ +.menu-status { + margin-right: 20rpx; + color: rgb(255, 255, 255); +} +.status-text { + font-size: 26rpx; + padding: 5rpx 15rpx; + border-radius: 20rpx; +} +.safe { + color: #dadada; + background-color: rgba(76, 175, 80, 0.1); /* 浅色背景 */ +} + +/* 菜单箭头 */ +.menu-arrow { + color: #666666; + font-size: 30rpx; +} + +/* 退出登录按钮 */ +.logout-section { + padding: 40rpx 30rpx; +} + +.logout-btn { + width: 100%; + height: 90rpx; + line-height: 90rpx; + /* 深蓝色到深灰色的渐变背景 */ + background: linear-gradient(135deg, #044db4 0%, #156301 100%); + color: #ffffff; + font-size: 32rpx; + border-radius: 45rpx; + text-align: center; + padding: 0; + border: none; + outline: none; + /* 沙粒纹理效果 */ + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%23ffffff' fill-opacity='0.05' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E"); + /* 增加质感和深度的阴影 */ + box-shadow: 0 4px 6px -1px rgb(48, 51, 238), + inset 0 1px 0 rgb(255, 0, 0); + /* 过渡动画效果 */ + transition: all 0.2s ease; +} + +/* 点击效果 */ +.logout-btn:active { + /* 点击时略微缩小 */ + transform: scale(0.98); + /* 加深背景色 */ + background: linear-gradient(135deg, #1d69d3 0%, #273140 100%); + /* 增强阴影 */ + box-shadow: 0 2px 4px -1px rgba(250, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +/* 底部安全区域(适配全面屏底部) */ +.bottom-space { + height: 60rpx; +} \ No newline at end of file diff --git a/pages/social/friend-detail/friend-detail.js b/pages/social/friend-detail/friend-detail.js new file mode 100644 index 0000000..8c5d832 --- /dev/null +++ b/pages/social/friend-detail/friend-detail.js @@ -0,0 +1,253 @@ +// 好友详情页面 +const app = getApp(); +const friendAPI = require('../../../utils/friend-api.js'); +const { initPageSystemInfo } = require('../../../utils/system-info-modern.js'); + +Page({ + data: { + // 好友信息 + friendInfo: null, + loading: true, + + // 系统信息 + statusBarHeight: 0, + navBarHeight: 0, + + // 操作状态 + isDeleting: false + }, + + onLoad(options) { + console.log('好友详情页面加载:', options); + + // 初始化系统信息 + this.initSystemInfo(); + + // 获取好友ID + const customId = options.customId; + if (customId) { + this.loadFriendDetail(customId); + } else { + wx.showToast({ + title: '参数错误', + icon: 'none' + }); + setTimeout(() => { + wx.navigateBack(); + }, 1500); + } + }, + + // 初始化系统信息 + initSystemInfo() { + const pageSystemInfo = initPageSystemInfo(); + this.setData({ + statusBarHeight: pageSystemInfo.statusBarHeight, + navBarHeight: pageSystemInfo.navBarHeight + }); + }, + + // 加载好友详情 + async loadFriendDetail(customId) { + try { + this.setData({ loading: true }); + + console.log('🔥 加载好友详情:', customId); + const response = await friendAPI.getFriendDetail(customId); + + if (response && response.code === 0) { + const friendInfo = response.data; + console.log('✅ 获取好友详情成功:', friendInfo); + + this.setData({ + friendInfo: friendInfo, + loading: false + }); + } else { + throw new Error(response?.message || '获取好友详情失败'); + } + + } catch (error) { + console.error('❌ 加载好友详情失败:', error); + this.setData({ loading: false }); + + wx.showToast({ + title: error.message || '加载失败', + icon: 'none' + }); + + // 延迟返回上一页 + setTimeout(() => { + wx.navigateBack(); + }, 1500); + } + }, + + // 返回上一页 + goBack() { + wx.navigateBack(); + }, + + // 发送消息 + sendMessage() { + const { friendInfo } = this.data; + if (!friendInfo) return; + + console.log('💬 发送消息给:', friendInfo.nickname); + + // 跳转到聊天页面 + const currentUserId = app.globalData.userInfo?.user?.customId || ''; + // 🔥 修复:不传递conversationId,让聊天页面从API获取正确的会话ID + + wx.navigateTo({ + url: `/pages/message/chat/chat?targetId=${friendInfo.customId}&name=${encodeURIComponent(friendInfo.nickname)}&chatType=0` + }); + }, + + // 视频通话 + videoCall() { + const { friendInfo } = this.data; + if (!friendInfo) return; + + console.log('📹 视频通话:', friendInfo.nickname); + wx.showToast({ + title: '视频通话功能开发中', + icon: 'none' + }); + }, + + // 设置备注 + setRemark() { + const { friendInfo } = this.data; + if (!friendInfo) return; + + wx.showModal({ + title: '设置备注', + editable: true, + placeholderText: '请输入备注名', + content: friendInfo.remark || '', + success: async (res) => { + if (res.confirm && res.content !== friendInfo.remark) { + try { + wx.showLoading({ title: '设置中...' }); + + // 这里需要调用更新好友备注的API + // await friendAPI.updateFriendRemark(friendInfo.customId, res.content); + + // 更新本地数据 + this.setData({ + 'friendInfo.remark': res.content + }); + + wx.hideLoading(); + wx.showToast({ + title: '设置成功', + icon: 'success' + }); + + } catch (error) { + wx.hideLoading(); + wx.showToast({ + title: '设置失败', + icon: 'none' + }); + } + } + } + }); + }, + + // 删除好友 + deleteFriend() { + const { friendInfo } = this.data; + if (!friendInfo) return; + + wx.showModal({ + title: '删除好友', + content: `确定要删除好友"${friendInfo.nickname}"吗?删除后将无法收到对方消息。`, + confirmText: '删除', + confirmColor: '#ff4757', + success: async (res) => { + if (res.confirm) { + try { + this.setData({ isDeleting: true }); + wx.showLoading({ title: '删除中...' }); + + console.log('🗑️ 删除好友:', friendInfo.customId); + await friendAPI.deleteFriend(friendInfo.customId); + + wx.hideLoading(); + wx.showToast({ + title: '已删除好友', + icon: 'success' + }); + + // 延迟返回并刷新好友列表 + setTimeout(() => { + // 通知好友页面刷新 + const pages = getCurrentPages(); + const friendsPage = pages.find(page => page.route === 'pages/social/friends/friends'); + if (friendsPage && friendsPage.loadFriends) { + friendsPage.loadFriends(); + } + + wx.navigateBack(); + }, 1000); + + } catch (error) { + console.error('❌ 删除好友失败:', error); + wx.hideLoading(); + this.setData({ isDeleting: false }); + + wx.showToast({ + title: error.message || '删除失败', + icon: 'none' + }); + } + } + } + }); + }, + + // 更多操作 + showMoreActions() { + const { friendInfo } = this.data; + if (!friendInfo) return; + + const actions = ['设置备注', '删除好友']; + + wx.showActionSheet({ + itemList: actions, + success: (res) => { + switch(res.tapIndex) { + case 0: + this.setRemark(); + break; + case 1: + this.deleteFriend(); + break; + } + } + }); + }, + + // 查看位置 + viewLocation() { + const { friendInfo } = this.data; + if (!friendInfo || !friendInfo.locationInfo) { + wx.showToast({ + title: '暂无位置信息', + icon: 'none' + }); + return; + } + + const { locationInfo } = friendInfo; + wx.openLocation({ + latitude: locationInfo.latitude, + longitude: locationInfo.longitude, + name: friendInfo.nickname, + address: locationInfo.address || '未知位置' + }); + } +}); \ No newline at end of file diff --git a/pages/social/friend-detail/friend-detail.json b/pages/social/friend-detail/friend-detail.json new file mode 100644 index 0000000..676b89c --- /dev/null +++ b/pages/social/friend-detail/friend-detail.json @@ -0,0 +1,4 @@ +{ + "navigationStyle": "custom", + "backgroundColor": "#f5f5f5" +} \ No newline at end of file diff --git a/pages/social/friend-detail/friend-detail.wxml b/pages/social/friend-detail/friend-detail.wxml new file mode 100644 index 0000000..3c4827f --- /dev/null +++ b/pages/social/friend-detail/friend-detail.wxml @@ -0,0 +1,178 @@ + + + + + + + + + + + 好友详情 + + + + + + + + + + + 加载中... + + + + + + + + + + + + + + {{friendInfo.nickname ? friendInfo.nickname.charAt(0) : '?'}} + + + + + + VIP{{friendInfo.memberLevel}} + + + + + + {{friendInfo.remark || friendInfo.nickname}} + + {{friendInfo.gender === 1 ? '♂' : '♀'}} + + + + ID: {{friendInfo.customId}} + + {{friendInfo.bio}} + + + + {{friendInfo.friendSince}} + 已成为好友 {{friendInfo.friendshipDays}} 天 + + + + + + + + 💬 + 发消息 + + + + 📹 + 视频通话 + + + + + + + + + + + 好友信息 + + + + + 备注 + {{friendInfo.remark}} + + + + 关系 + {{friendInfo.relation}} + + + + 分组 + {{friendInfo.group}} + + + + 手机号 + {{friendInfo.phone}} + + + + + + + + 位置信息 + + + + + 📍 + + {{friendInfo.locationInfo.address}} + 更新时间: {{friendInfo.lastLocationTime}} + + + + + + + + + + 设置 + + + + + + ✏️ + 设置备注和标签 + + + + + + + 🔕 + 消息免打扰 + + + + + + + 📌 + 置顶聊天 + + + + + + + + + + 🗑️ + 删除好友 + + + + + + + + \ No newline at end of file diff --git a/pages/social/friend-detail/friend-detail.wxss b/pages/social/friend-detail/friend-detail.wxss new file mode 100644 index 0000000..d50df47 --- /dev/null +++ b/pages/social/friend-detail/friend-detail.wxss @@ -0,0 +1,396 @@ +/* 好友详情页面样式 */ +.friend-detail-container { + min-height: 100vh; + background-color: #f5f5f5; +} + +/* 自定义导航栏 */ +.custom-nav-bar { + background-color: #fff; + border-bottom: 1px solid #e5e5e5; +} + +.nav-content { + display: flex; + align-items: center; + justify-content: space-between; + height: 44px; + padding: 0 16px; +} + +.nav-left, .nav-right { + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; +} + +.back-icon, .more-icon { + font-size: 20px; + color: #333; +} + +.nav-title { + font-size: 17px; + font-weight: 600; + color: #333; +} + +/* 加载状态 */ +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 200px; +} + +.loading-spinner { + width: 30px; + height: 30px; + border: 3px solid #f3f3f3; + border-top: 3px solid #007aff; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-text { + margin-top: 12px; + font-size: 14px; + color: #999; +} + +/* 详情内容 */ +.detail-content { + height: calc(100vh - 88px); + padding: 16px; +} + +/* 信息卡片 */ +.info-card { + background-color: #fff; + border-radius: 12px; + padding: 20px; + margin-bottom: 16px; +} + +.basic-info { + display: flex; + margin-bottom: 20px; +} + +.avatar-section { + position: relative; + margin-right: 16px; +} + +.friend-avatar { + width: 80px; + height: 80px; + border-radius: 12px; + overflow: hidden; +} + +.avatar-image { + width: 100%; + height: 100%; +} + +.avatar-placeholder { + width: 100%; + height: 100%; + background-color: #e5e5e5; + display: flex; + align-items: center; + justify-content: center; +} + +.avatar-text { + font-size: 32px; + color: #999; + font-weight: 500; +} + +.member-badge { + position: absolute; + bottom: -4px; + right: -4px; + background: linear-gradient(45deg, #ff6b6b, #ffa500); + border-radius: 8px; + padding: 2px 6px; +} + +.member-text { + font-size: 10px; + color: #fff; + font-weight: 600; +} + +.info-section { + flex: 1; +} + +.name-row { + display: flex; + align-items: center; + margin-bottom: 8px; +} + +.friend-name { + font-size: 20px; + font-weight: 600; + color: #333; + margin-right: 8px; +} + +.gender-icon { + width: 20px; + height: 20px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background-color: #007aff; +} + +.gender-text { + font-size: 12px; + color: #fff; +} + +.friend-id { + font-size: 14px; + color: #999; + margin-bottom: 8px; +} + +.friend-bio { + font-size: 14px; + color: #666; + line-height: 1.4; + margin-bottom: 12px; +} + +.friendship-info { + display: flex; + flex-direction: column; +} + +.friendship-text { + font-size: 13px; + color: #007aff; + margin-bottom: 4px; +} + +.friendship-days { + font-size: 12px; + color: #999; +} + +/* 操作按钮 */ +.action-buttons { + display: flex; + gap: 12px; +} + +.action-btn { + flex: 1; + height: 44px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; +} + +.action-btn.primary { + background-color: #007aff; +} + +.action-btn.secondary { + background-color: #f0f0f0; +} + +.btn-icon { + font-size: 16px; +} + +.btn-text { + font-size: 15px; + font-weight: 500; +} + +.action-btn.primary .btn-text { + color: #fff; +} + +.action-btn.secondary .btn-text { + color: #333; +} + +/* 详细信息区域 */ +.detail-sections { + display: flex; + flex-direction: column; + gap: 16px; +} + +.detail-section { + background-color: #fff; + border-radius: 12px; + overflow: hidden; +} + +.section-header { + padding: 16px 16px 8px 16px; +} + +.section-title { + font-size: 16px; + font-weight: 600; + color: #333; +} + +/* 信息项 */ +.info-items { + padding: 0 16px 16px 16px; +} + +.info-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #f0f0f0; +} + +.info-item:last-child { + border-bottom: none; +} + +.item-label { + font-size: 15px; + color: #666; +} + +.item-value { + font-size: 15px; + color: #333; + text-align: right; +} + +/* 位置信息 */ +.location-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; +} + +.location-info { + display: flex; + align-items: center; + flex: 1; +} + +.location-icon { + font-size: 16px; + margin-right: 12px; +} + +.location-details { + display: flex; + flex-direction: column; +} + +.location-address { + font-size: 15px; + color: #333; + margin-bottom: 4px; +} + +.location-time { + font-size: 12px; + color: #999; +} + +.location-arrow { + font-size: 16px; + color: #c7c7cc; +} + +/* 设置项 */ +.setting-items { + padding: 0 16px 16px 16px; +} + +.setting-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 0; + border-bottom: 1px solid #f0f0f0; +} + +.setting-item:last-child { + border-bottom: none; +} + +.setting-info { + display: flex; + align-items: center; + flex: 1; +} + +.setting-icon { + font-size: 16px; + margin-right: 12px; +} + +.setting-label { + font-size: 15px; + color: #333; +} + +.setting-arrow { + font-size: 16px; + color: #c7c7cc; +} + +.setting-switch { + transform: scale(0.8); +} + +/* 危险操作 */ +.danger-section { + background-color: #fff; +} + +.danger-item { + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + gap: 8px; +} + +.danger-icon { + font-size: 16px; +} + +.danger-text { + font-size: 15px; + color: #ff3b30; + font-weight: 500; +} + +/* 底部安全区域 */ +.safe-area-bottom { + height: 34px; +} \ No newline at end of file diff --git a/pages/social/friend-requests/friend-requests.js b/pages/social/friend-requests/friend-requests.js new file mode 100644 index 0000000..63eeb10 --- /dev/null +++ b/pages/social/friend-requests/friend-requests.js @@ -0,0 +1,248 @@ +// 好友请求管理页面 +const app = getApp(); +const friendAPI = require('../../../utils/friend-api.js'); + +Page({ + data: { + // 请求列表 + friendRequests: [], + pendingRequests: [], + processedRequests: [], + loading: true, + refreshing: false, + + // 统计 + pendingCount: 0, + processedCount: 0, + + // 系统信息 + statusBarHeight: 0, + navBarHeight: 0, + + // 标签页 + activeTab: 'pending', // pending, processed + tabs: [ + { key: 'pending', label: '待处理', count: 0 }, + { key: 'processed', label: '已处理', count: 0 } + ] + }, + + onLoad(options) { + console.log('好友请求页面加载'); + this.initSystemInfo(); + this.loadFriendRequests(); + }, + + onShow() { + // 刷新数据 + this.loadFriendRequests(); + }, + + // 初始化系统信息 + initSystemInfo() { + const systemInfo = wx.getSystemInfoSync(); + const menuButton = wx.getMenuButtonBoundingClientRect(); + + this.setData({ + statusBarHeight: systemInfo.statusBarHeight, + navBarHeight: menuButton.bottom + 10 + }); + }, + + // 返回上一页 + goBack() { + wx.navigateBack(); + }, + + // 加载好友请求 + async loadFriendRequests() { + try { + this.setData({ loading: true }); + + const response = await friendAPI.getFriendRequests(); + + // 从API响应中提取数据数组 + const requests = response.data || []; + + // 分类请求 + const pendingRequests = requests.filter(req => req.status === 0); + const processedRequests = requests.filter(req => req.status !== 0); + + // 更新标签页计数 + const updatedTabs = this.data.tabs.map(tab => ({ + ...tab, + count: tab.key === 'pending' ? pendingRequests.length : processedRequests.length + })); + + this.setData({ + friendRequests: requests, + pendingRequests: pendingRequests, + processedRequests: processedRequests, + pendingCount: pendingRequests.length, + processedCount: processedRequests.length, + tabs: updatedTabs, + loading: false, + refreshing: false + }); + + } catch (error) { + console.error('加载好友请求失败:', error); + this.setData({ + loading: false, + refreshing: false + }); + + wx.showToast({ + title: '加载失败', + icon: 'none' + }); + } + }, + + // 下拉刷新 + onRefresh() { + this.setData({ refreshing: true }); + this.loadFriendRequests(); + // 刷新后也通知好友页面更新数量 + setTimeout(() => { + this.notifyFriendsPageRefresh(); + }, 500); // 延迟一点确保数据加载完成 + }, + + // 切换标签页 + switchTab(e) { + const tab = e.currentTarget.dataset.tab; + this.setData({ + activeTab: tab + }); + + console.log('切换到标签页:', tab); + console.log('待处理请求数量:', this.data.pendingRequests?.length || 0); + console.log('已处理请求数量:', this.data.processedRequests?.length || 0); + }, + + // 获取当前标签页的请求列表 + getCurrentRequests() { + const { friendRequests, activeTab } = this.data; + + if (activeTab === 'pending') { + return friendRequests.filter(req => req.status === 0); + } else { + return friendRequests.filter(req => req.status !== 0); + } + }, + + // 处理好友请求 + async handleRequest(e) { + const { requestId, accept } = e.currentTarget.dataset; + const actionText = accept === 'true' ? '接受' : '拒绝'; + + try { + wx.showLoading({ title: `${actionText}中...` }); + + await friendAPI.handleFriendRequest(requestId, accept === 'true'); + + wx.hideLoading(); + wx.showToast({ + title: `已${actionText}`, + icon: 'success' + }); + + // 刷新列表 + this.loadFriendRequests(); + + // 通知好友页面刷新请求数量 + this.notifyFriendsPageRefresh(); + + } catch (error) { + wx.hideLoading(); + console.error('处理好友请求失败:', error); + wx.showToast({ + title: error.message || `${actionText}失败`, + icon: 'none' + }); + } + }, + + // 查看用户详情 + viewUserDetail(e) { + const { customId } = e.currentTarget.dataset; + wx.navigateTo({ + url: `/pages/social/user-detail/user-detail?customId=${customId}` + }); + }, + + // 通知好友页面刷新 + notifyFriendsPageRefresh() { + try { + // 通过全局事件通知好友页面刷新 + const app = getApp(); + if (app.globalData) { + app.globalData.needRefreshFriendRequests = true; + } + + // 也可以通过页面栈找到好友页面并直接调用刷新方法 + const pages = getCurrentPages(); + const friendsPage = pages.find(page => page.route === 'pages/social/friends/friends'); + if (friendsPage && friendsPage.loadFriendRequestsCount) { + friendsPage.loadFriendRequestsCount(); + } + + console.log('✅ 已通知好友页面刷新请求数量'); + } catch (error) { + console.error('❌ 通知好友页面失败:', error); + } + }, + + // 发送消息 + sendMessage(e) { + const { customId, nickname } = e.currentTarget.dataset; + wx.navigateTo({ + url: `/pages/message/chat/chat?targetId=${customId}&name=${encodeURIComponent(nickname)}&chatType=0` + }); + }, + + // 格式化时间 + formatTime(timeStr) { + const time = new Date(timeStr); + const now = new Date(); + const diff = now - time; + + const minute = 60 * 1000; + const hour = 60 * minute; + const day = 24 * hour; + const week = 7 * day; + + if (diff < minute) { + return '刚刚'; + } else if (diff < hour) { + return `${Math.floor(diff / minute)}分钟前`; + } else if (diff < day) { + return `${Math.floor(diff / hour)}小时前`; + } else if (diff < week) { + return `${Math.floor(diff / day)}天前`; + } else { + return time.toLocaleDateString(); + } + }, + + // 获取状态文本 + getStatusText(status) { + switch (status) { + case 0: return '待处理'; + case 1: return '已接受'; + case 2: return '已拒绝'; + default: return '未知'; + } + }, + + // 获取状态样式类 + getStatusClass(status) { + switch (status) { + case 0: return 'pending'; + case 1: return 'accepted'; + case 2: return 'rejected'; + default: return 'unknown'; + } + } +}); diff --git a/pages/social/friend-requests/friend-requests.wxml b/pages/social/friend-requests/friend-requests.wxml new file mode 100644 index 0000000..3ab7059 --- /dev/null +++ b/pages/social/friend-requests/friend-requests.wxml @@ -0,0 +1,190 @@ + + + + + + + + + + + 好友请求 + + + + + + + + + {{item.label}} + + {{item.count > 99 ? '99+' : item.count}} + + + + + + + + + + + 加载中... + + + + + + {{activeTab === 'pending' ? '📭' : '📋'}} + + {{activeTab === 'pending' ? '暂无待处理请求' : '暂无已处理记录'}} + {{activeTab === 'pending' ? '当有人向您发送好友请求时,会在这里显示' : '您处理过的好友请求会在这里显示'}} + + + + + + + + + + + + + {{item.senderNickname ? item.senderNickname.charAt(0) : '?'}} + + + + + + + + + + + + 拒绝 + + + + 接受 + + + + + + + + 发消息 + + + + + + + + + + + + var getCurrentRequests = function(friendRequests, activeTab) { + if (!friendRequests || !friendRequests.length) return []; + + if (activeTab === 'pending') { + return friendRequests.filter(function(req) { + return req.status === 0; + }); + } else { + return friendRequests.filter(function(req) { + return req.status !== 0; + }); + } + }; + + var formatTime = function(timeStr) { + if (!timeStr) return ''; + + var time = getDate(timeStr); + var now = getDate(); + var diff = now.getTime() - time.getTime(); + + var minute = 60 * 1000; + var hour = 60 * minute; + var day = 24 * hour; + var week = 7 * day; + + if (diff < minute) { + return '刚刚'; + } else if (diff < hour) { + return Math.floor(diff / minute) + '分钟前'; + } else if (diff < day) { + return Math.floor(diff / hour) + '小时前'; + } else if (diff < week) { + return Math.floor(diff / day) + '天前'; + } else { + return time.toLocaleDateString(); + } + }; + + var getStatusText = function(status) { + if (status === 0) return '待处理'; + if (status === 1) return '已接受'; + if (status === 2) return '已拒绝'; + return '未知'; + }; + + var getStatusClass = function(status) { + if (status === 0) return 'pending'; + if (status === 1) return 'accepted'; + if (status === 2) return 'rejected'; + return 'unknown'; + }; + + module.exports = { + getCurrentRequests: getCurrentRequests, + formatTime: formatTime, + getStatusText: getStatusText, + getStatusClass: getStatusClass + }; + diff --git a/pages/social/friend-requests/friend-requests.wxss b/pages/social/friend-requests/friend-requests.wxss new file mode 100644 index 0000000..8a60c4e --- /dev/null +++ b/pages/social/friend-requests/friend-requests.wxss @@ -0,0 +1,353 @@ +/* 好友请求管理页面样式 */ + +.requests-container { + height: 100vh; + background-color: #f8f9fa; + display: flex; + flex-direction: column; +} + +/* 自定义导航栏 */ +.custom-nav-bar { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; +} + +.nav-content { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32rpx; + height: 88rpx; +} + +.nav-left { + width: 80rpx; + display: flex; + align-items: center; +} + +.back-icon { + font-size: 40rpx; + color: white; + font-weight: bold; +} + +.nav-center { + flex: 1; + text-align: center; +} + +.nav-title { + font-size: 36rpx; + font-weight: 600; + color: white; +} + +.nav-right { + width: 80rpx; +} + +/* 标签页 */ +.tabs-container { + margin-top: 176rpx; + background: white; + display: flex; + border-bottom: 1px solid #f0f0f0; +} + +.tab-item { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 32rpx 0; + position: relative; + gap: 16rpx; +} + +.tab-item.active { + border-bottom: 4rpx solid #667eea; +} + +.tab-text { + font-size: 32rpx; + color: #666; + font-weight: 500; +} + +.tab-item.active .tab-text { + color: #667eea; + font-weight: 600; +} + +.tab-badge { + background: #ff4757; + border-radius: 20rpx; + padding: 4rpx 12rpx; + min-width: 32rpx; + height: 32rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.badge-text { + font-size: 20rpx; + color: white; + font-weight: 600; +} + +/* 内容区域 */ +.content-container { + flex: 1; + padding: 0 32rpx; +} + +/* 加载中 */ +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 120rpx 0; +} + +.loading-spinner { + width: 60rpx; + height: 60rpx; + border: 4rpx solid #f3f3f3; + border-top: 4rpx solid #667eea; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 24rpx; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-text { + font-size: 28rpx; + color: #999; +} + +/* 空状态 */ +.empty-container { + display: flex; + flex-direction: column; + align-items: center; + padding: 120rpx 0; + text-align: center; +} + +.empty-icon { + font-size: 120rpx; + margin-bottom: 32rpx; + opacity: 0.6; +} + +.empty-title { + font-size: 36rpx; + font-weight: 600; + color: #333; + margin-bottom: 16rpx; +} + +.empty-desc { + font-size: 28rpx; + color: #666; + line-height: 1.5; +} + +/* 请求列表 */ +.requests-list { + padding: 32rpx 0; +} + +.request-item { + background: white; + border-radius: 24rpx; + padding: 32rpx; + margin-bottom: 24rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); +} + +/* 用户信息区域 */ +.user-section { + display: flex; + align-items: flex-start; + margin-bottom: 24rpx; +} + +.user-avatar { + margin-right: 24rpx; +} + +.avatar-image { + width: 96rpx; + height: 96rpx; + border-radius: 48rpx; +} + +.avatar-placeholder { + width: 96rpx; + height: 96rpx; + border-radius: 48rpx; + background: #e0e0e0; + display: flex; + align-items: center; + justify-content: center; +} + +.avatar-text { + font-size: 36rpx; + font-weight: 600; + color: #666; +} + +.user-info { + flex: 1; +} + +.user-name { + display: flex; + align-items: center; + margin-bottom: 8rpx; + gap: 16rpx; +} + +.nickname { + font-size: 32rpx; + font-weight: 600; + color: #333; +} + +.status-badge { + padding: 4rpx 12rpx; + border-radius: 12rpx; + font-size: 22rpx; +} + +.status-badge.pending { + background: #fff3e0; + color: #ff9800; +} + +.status-badge.accepted { + background: #e8f5e8; + color: #4caf50; +} + +.status-badge.rejected { + background: #ffebee; + color: #f44336; +} + +.status-text { + font-weight: 600; +} + +.user-id { + font-size: 24rpx; + color: #999; + margin-bottom: 16rpx; +} + +.request-message { + background: #f8f9fa; + border-radius: 12rpx; + padding: 16rpx; + margin-bottom: 16rpx; +} + +.message-text { + font-size: 28rpx; + color: #333; + line-height: 1.4; +} + +.time-info { + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.time-text { + font-size: 24rpx; + color: #999; +} + +.handle-time { + font-size: 22rpx; + color: #ccc; +} + +/* 操作按钮区域 */ +.action-section { + border-top: 1px solid #f0f0f0; + padding-top: 24rpx; +} + +.action-buttons { + display: flex; + gap: 24rpx; +} + +.action-btn { + flex: 1; + padding: 20rpx 0; + border-radius: 50rpx; + text-align: center; + font-weight: 600; +} + +.reject-btn { + background: #f8f9fa; + border: 2rpx solid #e0e0e0; +} + +.reject-btn .btn-text { + color: #666; + font-size: 28rpx; +} + +.accept-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.accept-btn .btn-text { + color: white; + font-size: 28rpx; +} + +/* 已处理状态的操作 */ +.processed-actions { + border-top: 1px solid #f0f0f0; + padding-top: 24rpx; + display: flex; + justify-content: flex-end; +} + +.message-btn { + background: #e3f2fd; + border: 2rpx solid #2196f3; + border-radius: 50rpx; + padding: 16rpx 32rpx; +} + +.message-btn-text { + color: #2196f3; + font-size: 26rpx; + font-weight: 600; +} + +/* 底部安全区域 */ +.safe-area-bottom { + height: 60rpx; +} diff --git a/pages/social/friends/friends.js b/pages/social/friends/friends.js new file mode 100644 index 0000000..11ee676 --- /dev/null +++ b/pages/social/friends/friends.js @@ -0,0 +1,959 @@ +// 好友列表页面 +const app = getApp(); +const apiClient = require('../../../utils/api-client.js'); +const friendAPI = require('../../../utils/friend-api.js'); +const wsManager = require('../../../utils/websocket-manager-v2.js'); +const { modernSystemInfo, initPageSystemInfo } = require('../../../utils/system-info-modern.js'); + +// 事件处理工具函数 +const EventUtils = { + // 安全地阻止事件冒泡 + safeStopPropagation(event) { + try { + if (event && typeof event.stopPropagation === 'function') { + event.stopPropagation(); + } + } catch (error) { + console.warn('停止事件冒泡失败:', error); + } + }, + + // 安全地阻止默认行为 + safePreventDefault(event) { + try { + if (event && typeof event.preventDefault === 'function') { + event.preventDefault(); + } + } catch (error) { + console.warn('阻止默认行为失败:', error); + } + }, + + // 安全地从事件中提取数据 + safeGetDataset(event) { + try { + return event?.currentTarget?.dataset || event?.target?.dataset || {}; + } catch (error) { + console.warn('获取事件数据失败:', error); + return {}; + } + }, + + // 验证好友数据 + validateFriendData(friend) { + return friend && + friend.customId && + friend.nickname && + typeof friend === 'object'; + } +}; + +Page({ + data: { + // 好友数据 + friends: [], + filteredFriends: [], + + // UI状态 + loading: true, + refreshing: false, + searchKeyword: '', + + // 统计数据 + newFriendRequests: 0, + totalFriendsCount: 0, + onlineFriendsCount: 0, + recentActiveCount: 0, + mutualFriendsCount: 0, + + // 系统适配信息 + systemInfo: {}, + statusBarHeight: 0, + menuButtonHeight: 0, + menuButtonTop: 0, + navBarHeight: 0, + windowHeight: 0, + safeAreaBottom: 0, + + // 添加好友提示 + showAddTip: false, + userInfo: {}, + + // 事件处理状态 + eventHandlingState: { + lastEventTime: 0, + eventThrottleMs: 300, // 防止快速连续点击 + errorCount: 0, + maxErrors: 5 + } + }, + + onLoad: function (options) { + console.log('好友列表页面加载'); + this.initSystemInfo(); + this.checkAuthAndLoad(); + }, + + onShow: function () { + console.log('好友列表页面显示'); + + // 检查是否需要刷新好友请求数量 + const app = getApp(); + if (app.globalData && app.globalData.needRefreshFriendRequests) { + console.log('🔄 检测到好友请求状态变化,强制刷新'); + app.globalData.needRefreshFriendRequests = false; + } + + // 刷新数据 + this.loadFriends(); + this.loadFriendRequestsCount(); + }, + + // 初始化系统信息 + // 初始化系统信息 - 使用现代化API + initSystemInfo() { + const pageSystemInfo = initPageSystemInfo(); + + console.log('系统适配信息:', { + statusBarHeight: pageSystemInfo.statusBarHeight, + menuButtonHeight: pageSystemInfo.menuButtonHeight, + menuButtonTop: pageSystemInfo.menuButtonTop, + navBarHeight: pageSystemInfo.navBarHeight, + windowHeight: pageSystemInfo.windowHeight, + safeAreaBottom: pageSystemInfo.safeAreaBottom, + screenWidth: pageSystemInfo.systemInfo.screenWidth, + screenHeight: pageSystemInfo.systemInfo.screenHeight, + platform: pageSystemInfo.systemInfo.platform + }); + + this.setData({ + systemInfo: pageSystemInfo.systemInfo, + statusBarHeight: pageSystemInfo.statusBarHeight, + menuButtonHeight: pageSystemInfo.menuButtonHeight, + menuButtonTop: pageSystemInfo.menuButtonTop, + navBarHeight: pageSystemInfo.navBarHeight, + windowHeight: pageSystemInfo.windowHeight, + safeAreaBottom: pageSystemInfo.safeAreaBottom + }); + }, + + // 检查认证状态并加载数据 + async checkAuthAndLoad() { + try { + // 确保API客户端能获取到token + const currentToken = apiClient.getToken(); + if (!currentToken) { + console.error('用户未登录,跳转到登录页'); + wx.reLaunch({ + url: '/pages/login/login' + }); + return; + } + + console.log('好友页面认证检查通过,开始加载数据'); + + // 获取用户信息 - 确保有完整的用户数据 + await this.loadUserInfo(); + + // 🔥 初始化WebSocket好友功能 + this.initWebSocketFriendFeatures(); + + // 开始加载数据 + this.loadFriends(); + this.loadFriendRequestsCount(); + + } catch (error) { + console.error('认证检查失败:', error); + wx.reLaunch({ + url: '/pages/login/login' + }); + } + }, + + // 加载用户信息 + async loadUserInfo() { + try { + // 先从全局数据获取 + let userInfo = getApp().globalData.userInfo; + + // 如果全局没有完整信息,从API获取 + if (!userInfo || !userInfo.user || !userInfo.user.customId) { + console.log('从API获取用户信息...'); + const response = await apiClient.getUserInfo(); + if (response && response.code === 0) { + userInfo = { + ...userInfo, + user: response.data + }; + // 更新全局数据 + getApp().globalData.userInfo = userInfo; + } + } + + this.setData({ + userInfo: userInfo + }); + + console.log('✅ 用户信息已更新:', { + hasUser: !!userInfo?.user, + customId: userInfo?.user?.customId || 'unknown' + }); + + } catch (error) { + console.error('❌ 获取用户信息失败:', error); + // 不影响主要功能,继续加载好友列表 + } + }, + + // 加载好友列表 - 参考Flutter app的实现 + async loadFriends() { + try { + this.setData({ loading: true }); + + console.log('🔥 开始加载好友列表...'); + const response = await friendAPI.getFriendList(); + + if (response && response.code === 0) { + const friends = response.data || []; + console.log(`✅ 获取到 ${friends.length} 个好友:`, friends); + + // 处理好友数据,参考Flutter app的数据结构 + const processedFriends = this.processFriendsData(friends); + + this.setData({ + friends: processedFriends, + filteredFriends: processedFriends, + totalFriendsCount: processedFriends.length, + loading: false + }); + + // 计算在线好友数和其他统计 + this.calculateFriendStats(processedFriends); + + } else { + throw new Error(response?.message || '获取好友列表失败'); + } + + } catch (error) { + console.error('❌ 加载好友列表失败:', error); + this.setData({ loading: false }); + + // 不要频繁弹出错误提示,影响用户体验 + if (!error.message?.includes('401')) { + wx.showToast({ + title: '加载好友失败', + icon: 'none', + duration: 2000 + }); + } + } + }, + + // 处理好友数据 - 参考Flutter app的数据结构 + processFriendsData(friends) { + return friends.map(friend => { + // 适配不同的字段名,参考Flutter app的FriendModel + const nickname = friend.nickname || friend.username || friend.name || '未知用户'; + const customId = friend.customId || friend.customID || friend.id; + + return { + id: customId, + customId: customId, + name: nickname, + nickname: nickname, + avatar: friend.avatar || '', // 头像URL + personalSignature: friend.signature || friend.bio || friend.personalSignature || '', + isOnline: friend.isOnline || false, + isVip: friend.isVip || false, + gender: friend.gender || null, // male, female, null + remark: friend.remark || '', + relation: friend.relation || '好友', + location: friend.location || '', + distance: friend.distance || 0, + lastActiveTime: friend.lastActiveTime || '', + tags: friend.tags || [], + hasMutualFriends: friend.mutualFriends > 0, + isBirthdayToday: false, // 可以根据实际情况计算 + isNewFriend: friend.isNewFriend || false + }; + }); + }, + + // 计算好友统计数据 + calculateFriendStats(friends) { + const onlineCount = friends.filter(f => f.isOnline).length; + const recentActiveCount = friends.filter(f => { + if (!f.lastActiveTime) return false; + const oneHourAgo = Date.now() - (60 * 60 * 1000); + return new Date(f.lastActiveTime).getTime() > oneHourAgo; + }).length; + const mutualCount = friends.filter(f => f.hasMutualFriends).length; + + this.setData({ + onlineFriendsCount: onlineCount, + recentActiveCount: recentActiveCount, + mutualFriendsCount: mutualCount + }); + }, + + // 获取好友请求数量 - 参考Flutter app的实现 + async loadFriendRequestsCount() { + try { + console.log('🔥 获取好友请求数量...'); + const response = await friendAPI.getFriendRequestCount(); + + if (response && response.code === 0) { + const count = response.data?.count || 0; + console.log(`✅ 获取到 ${count} 个好友请求`); + this.setData({ + newFriendRequests: count + }); + } + } catch (error) { + console.error('❌ 获取好友请求数量失败:', error); + // 不影响主要功能,只是数量显示为0 + this.setData({ + newFriendRequests: 0 + }); + } + }, + + // 搜索输入 + onSearchInput(e) { + const keyword = e.detail.value; + this.setData({ searchKeyword: keyword }); + this.filterFriends(keyword); + }, + + // 过滤好友 + filterFriends(keyword) { + if (!keyword.trim()) { + this.setData({ filteredFriends: this.data.friends }); + return; + } + + const filtered = this.data.friends.filter(friend => { + const name = friend.remark || friend.nickname; + const signature = friend.personalSignature || ''; + const searchText = keyword.toLowerCase(); + + return name.toLowerCase().includes(searchText) || + signature.toLowerCase().includes(searchText); + }); + + this.setData({ filteredFriends: filtered }); + }, + + // 清除搜索 + clearSearch() { + this.setData({ searchKeyword: '' }); + this.filterFriends(''); + }, + + // 下拉刷新 + async onRefresh() { + console.log('下拉刷新好友列表'); + this.setData({ refreshing: true }); + + try { + await this.loadFriends(); + await this.loadFriendRequestsCount(); + + wx.showToast({ + title: '刷新成功', + icon: 'success', + duration: 1000 + }); + } catch (error) { + wx.showToast({ + title: '刷新失败', + icon: 'none' + }); + } finally { + this.setData({ refreshing: false }); + } + }, + + // 打开聊天 - WXML中引用的方法 + // 点击好友时查看个人资料 + openChat(e) { + try { + // 安全地获取好友数据 + const dataset = EventUtils.safeGetDataset(e); + const friend = dataset.friend; + + // 验证好友数据 + if (!EventUtils.validateFriendData(friend)) { + console.error('好友数据无效,无法查看个人资料'); + wx.showToast({ + title: '好友信息错误', + icon: 'none' + }); + return; + } + + console.log('查看好友个人资料:', friend.nickname); + + // 跳转到好友个人资料页面 + wx.navigateTo({ + url: `/pages/user-profile/user-profile?userId=${friend.customId}&from=friends` + }); + + } catch (error) { + console.error('查看好友个人资料失败:', error); + wx.showToast({ + title: '查看资料失败', + icon: 'none' + }); + } + }, + + // 打开好友资料 + openFriendProfile(e) { + const friend = e.currentTarget.dataset.friend; + console.log('打开好友资料:', friend.nickname); + + wx.navigateTo({ + url: `/pages/social/friend-detail/friend-detail?customId=${friend.customId}` + }); + }, + + // 发送消息 - 参考Flutter app的实现 + sendMessage(e) { + try { + // 安全地阻止事件冒泡 + EventUtils.safeStopPropagation(e); + + // 安全地获取好友数据 + const dataset = EventUtils.safeGetDataset(e); + const friend = dataset.friend; + + // 验证好友数据 + if (!EventUtils.validateFriendData(friend)) { + console.error('好友数据无效或缺失'); + wx.showToast({ + title: '好友信息错误', + icon: 'none' + }); + return; + } + + console.log('💬 开始与好友聊天:', friend.nickname); + + // 🔥 修复:不传递conversationId,让聊天页面从API获取正确的会话ID + wx.navigateTo({ + url: `/pages/message/chat/chat?targetId=${friend.customId}&name=${encodeURIComponent(friend.nickname)}&chatType=0` + }); + + } catch (error) { + console.error('发送消息失败:', error); + wx.showToast({ + title: '发送消息失败', + icon: 'none' + }); + } + }, + + // 视频通话 + startVideoCall(e) { + try { + // 安全地阻止事件冒泡 + EventUtils.safeStopPropagation(e); + + // 安全地获取好友数据 + const dataset = EventUtils.safeGetDataset(e); + const friend = dataset.friend; + + // 验证好友数据 + if (!EventUtils.validateFriendData(friend)) { + console.error('好友数据无效,无法发起视频通话'); + wx.showToast({ + title: '好友信息错误', + icon: 'none' + }); + return; + } + + console.log('📹 发起视频通话:', friend.nickname); + + wx.showToast({ + title: '视频通话功能开发中', + icon: 'none' + }); + + } catch (error) { + console.error('视频通话失败:', error); + wx.showToast({ + title: '视频通话失败', + icon: 'none' + }); + } + }, + + // 视频通话(WXML中使用的方法名) + videoCall(e) { + this.startVideoCall(e); + }, + + // 显示好友菜单 + showFriendMenu(e) { + const friend = e.currentTarget.dataset.friend; + console.log('长按好友:', friend.nickname); + + wx.showActionSheet({ + itemList: ['发送消息', '音视频通话', '查看资料', '设置备注', '删除好友'], + success: (res) => { + switch (res.tapIndex) { + case 0: + this.sendMessage({ currentTarget: { dataset: { friend } } }); + break; + case 1: + this.startVideoCall({ currentTarget: { dataset: { friend } } }); + break; + case 2: + this.openFriendProfile({ currentTarget: { dataset: { friend } } }); + break; + case 3: + this.setFriendRemark(friend); + break; + case 4: + this.deleteFriend(friend); + break; + } + } + }); + }, + + // 设置好友备注 + setFriendRemark(friend) { + wx.showModal({ + title: '设置备注', + editable: true, + placeholderText: '请输入备注名', + content: friend.remark || '', + success: (res) => { + if (res.confirm) { + console.log('设置备注:', res.content); + // 这里调用API更新备注 + } + } + }); + }, + + // 删除好友 + deleteFriend(friend) { + wx.showModal({ + title: '删除好友', + content: `确定要删除好友"${friend.nickname}"吗?`, + success: (res) => { + if (res.confirm) { + console.log('删除好友:', friend.nickname); + // 这里调用API删除好友 + } + } + }); + }, + + // 好友请求 - 修改后的名称 + openNewFriends() { + console.log('打开好友请求页面'); + wx.navigateTo({ + url: '/pages/social/friend-requests/friend-requests' + }); + }, + + // 建群 - 修改后的功能 + openGroupChats() { + console.log('打开建群页面'); + wx.navigateTo({ + url: '/pages/social/create-group/create-group' + }); + }, + + // 标签管理 + openTags() { + console.log('打开标签管理'); + wx.showToast({ + title: '标签功能开发中', + icon: 'none' + }); + }, + + // 添加好友 - 跳转到搜索页面 + addFriend() { + console.log('跳转到搜索用户页面'); + wx.navigateTo({ + url: '/pages/social/search/search' + }); + }, + + // 显示菜单 + showMenu() { + wx.showActionSheet({ + itemList: ['好友设置', '隐私设置', '黑名单管理', '好友分组', '数据统计'], + success: (res) => { + switch (res.tapIndex) { + case 0: + this.openFriendSettings(); + break; + case 1: + this.openPrivacySettings(); + break; + case 2: + this.openBlacklist(); + break; + case 3: + this.openFriendGroups(); + break; + case 4: + this.showFriendStats(); + break; + } + } + }); + }, + + // 好友设置 + openFriendSettings() { + wx.showToast({ + title: '好友设置功能开发中', + icon: 'none' + }); + }, + + // 隐私设置 + openPrivacySettings() { + wx.showToast({ + title: '隐私设置功能开发中', + icon: 'none' + }); + }, + + // 黑名单管理 + openBlacklist() { + wx.showToast({ + title: '黑名单管理功能开发中', + icon: 'none' + }); + }, + + // 好友分组 + openFriendGroups() { + wx.showToast({ + title: '好友分组功能开发中', + icon: 'none' + }); + }, + + // 好友统计 + showFriendStats() { + const stats = { + total: this.data.totalFriendsCount, + online: this.data.onlineFriendsCount, + recent: this.data.recentActiveCount, + mutual: this.data.mutualFriendsCount + }; + + wx.showModal({ + title: '好友统计', + content: `总好友数: ${stats.total}\n在线好友: ${stats.online}\n最近活跃: ${stats.recent}\n共同好友: ${stats.mutual}`, + showCancel: false + }); + }, + + // 扫码添加 + addByQR() { + this.setData({ showAddTip: false }); + wx.scanCode({ + success: (res) => { + console.log('扫码结果:', res.result); + // 处理扫码结果 + } + }); + }, + + // 搜索添加 + addBySearch() { + this.setData({ showAddTip: false }); + wx.navigateTo({ + url: '/pages/social/search-user/search-user' + }); + }, + + // 手机号添加 + addByPhone() { + this.setData({ showAddTip: false }); + wx.showToast({ + title: '手机号添加功能开发中', + icon: 'none' + }); + }, + + // 附近的人 + addByNearby() { + this.setData({ showAddTip: false }); + wx.navigateTo({ + url: '/pages/map/map' + }); + }, + + // 搜索好友 + searchFriends() { + wx.navigateTo({ + url: '/pages/social/search/search' + }); + }, + + // 扫描二维码 + scanQRCode() { + wx.scanCode({ + success: (res) => { + console.log('扫码结果:', res.result); + // 处理扫码结果 + } + }); + }, + + // 邀请好友 + inviteFriends() { + wx.showActionSheet({ + itemList: ['微信邀请', '短信邀请', '复制邀请链接'], + success: (res) => { + switch (res.tapIndex) { + case 0: + wx.showToast({ title: '微信邀请功能开发中', icon: 'none' }); + break; + case 1: + wx.showToast({ title: '短信邀请功能开发中', icon: 'none' }); + break; + case 2: + wx.setClipboardData({ + data: 'https://findme.app/invite', + success: () => { + wx.showToast({ title: '邀请链接已复制', icon: 'success' }); + } + }); + break; + } + } + }); + }, + + // 🔥 ===== 新增的好友功能方法 ===== + + // 初始化WebSocket好友功能 + initWebSocketFriendFeatures() { + try { + // 注册消息处理器 - 使用V2版本的统一API + wsManager.on('message', (message) => { + console.log('🔥 好友页面收到WebSocket消息:', message); + + // 根据消息类型分发处理 + switch (message.type) { + case 'friend_request': + this.handleNewFriendRequest(message.data); + break; + case 'friend_accepted': + this.handleFriendAccepted(message.data); + break; + case 'friend_rejected': + this.handleFriendRejected(message.data); + break; + case 'notification': + this.handleFriendNotification(message.data); + break; + default: + console.log('好友页面未处理的消息类型:', message.type); + } + }); + + } catch (error) { + console.error('初始化WebSocket好友功能失败:', error); + } + }, + + // 处理新的好友请求 + handleNewFriendRequest(data) { + try { + // 更新好友请求数量 + const currentCount = this.data.newFriendRequests; + this.setData({ + newFriendRequests: currentCount + 1 + }); + + // 显示通知 + wx.showToast({ + title: `${data.senderName} 请求添加您为好友`, + icon: 'none', + duration: 3000 + }); + + } catch (error) { + console.error('处理新好友请求失败:', error); + } + }, + + // 处理好友请求被接受 + handleFriendAccepted(data) { + try { + // 刷新好友列表 + this.loadFriends(); + + // 显示通知 + wx.showToast({ + title: `${data.friendName} 接受了您的好友请求`, + icon: 'success', + duration: 2000 + }); + + } catch (error) { + console.error('处理好友请求被接受失败:', error); + } + }, + + // 处理好友请求被拒绝 + handleFriendRejected(data) { + try { + // 显示通知 + wx.showToast({ + title: '好友请求被拒绝', + icon: 'none', + duration: 2000 + }); + + } catch (error) { + console.error('处理好友请求被拒绝失败:', error); + } + }, + + // 处理好友相关通知 + handleFriendNotification(data) { + try { + switch (data.type) { + case 'new_friend_request': + this.handleNewFriendRequest(data); + break; + case 'friend_accepted': + this.handleFriendAccepted(data); + break; + case 'friend_rejected': + this.handleFriendRejected(data); + break; + default: + console.log('未知好友通知类型:', data.type); + } + } catch (error) { + console.error('处理好友通知失败:', error); + } + }, + + // 🔥 ===== 好友操作增强 ===== + + // 删除好友 + async deleteFriend(friendId, friendName) { + try { + const result = await new Promise((resolve) => { + wx.showModal({ + title: '删除好友', + content: `确定要删除好友 ${friendName} 吗?`, + success: resolve + }); + }); + + if (!result.confirm) return; + + await friendAPI.deleteFriend(friendId); + + // 从本地数据中移除 + const friends = this.data.friends.filter(friend => friend.id !== friendId); + this.setData({ + friends: friends, + filteredFriends: friends, + totalFriendsCount: friends.length + }); + + // 重新计算统计数据 + this.calculateFriendStats(friends); + + wx.showToast({ + title: '已删除好友', + icon: 'success' + }); + + } catch (error) { + console.error('删除好友失败:', error); + wx.showToast({ + title: '删除失败', + icon: 'none' + }); + } + }, + + // 更新好友备注 + async updateFriendRemark(friendId, newRemark) { + try { + await friendAPI.updateFriendRelation(friendId, { + remark: newRemark + }); + + // 更新本地数据 + const friends = this.data.friends.map(friend => { + if (friend.id === friendId) { + return { ...friend, remark: newRemark }; + } + return friend; + }); + + this.setData({ + friends: friends, + filteredFriends: friends + }); + + wx.showToast({ + title: '备注已更新', + icon: 'success' + }); + + } catch (error) { + console.error('更新好友备注失败:', error); + wx.showToast({ + title: '更新失败', + icon: 'none' + }); + } + }, + + // 设置好友标签 + async setFriendLabel(friendId, label) { + try { + await friendAPI.updateFriendRelation(friendId, { + relation: label + }); + + // 更新本地数据 + const friends = this.data.friends.map(friend => { + if (friend.id === friendId) { + return { ...friend, relation: label }; + } + return friend; + }); + + this.setData({ + friends: friends, + filteredFriends: friends + }); + + wx.showToast({ + title: '标签已设置', + icon: 'success' + }); + + } catch (error) { + console.error('设置好友标签失败:', error); + wx.showToast({ + title: '设置失败', + icon: 'none' + }); + } + } +}); \ No newline at end of file diff --git a/pages/social/friends/friends.json b/pages/social/friends/friends.json new file mode 100644 index 0000000..0df89ae --- /dev/null +++ b/pages/social/friends/friends.json @@ -0,0 +1,7 @@ +{ + "navigationBarTitleText": "好友", + "navigationBarBackgroundColor": "#667eea", + "navigationBarTextStyle": "white", + "backgroundColor": "#f8f9fa", + "disableScroll": true +} \ No newline at end of file diff --git a/pages/social/friends/friends.wxml b/pages/social/friends/friends.wxml new file mode 100644 index 0000000..d63debe --- /dev/null +++ b/pages/social/friends/friends.wxml @@ -0,0 +1,125 @@ + + + + + + + + 好友 + {{totalFriendsCount}} 位联系人 + + + + + + + + + + + + + + + + + + + + + + + + + + + + 👥 + {{newFriendRequests}} + + + 新的朋友 + {{newFriendRequests}} 个新请求 + 暂无新请求 + + + + + + + 👨‍👩‍👧‍👦 + + + 群聊 + 查看群聊列表 + + + + + + + + + 联系人 + + + + + + + + + + {{item.nickname.charAt(0)}} + + + + + + + {{item.remark || item.nickname}} + {{item.personalSignature || (item.isOnline ? '在线' : '离线')}} + + + + + + 💬 + + + 📹 + + + + + + + + + 👥 + 还没有好友 + 点击右上角 ➕ 添加好友 + + 添加好友 + + + + + + + 加载中... + + + + \ No newline at end of file diff --git a/pages/social/friends/friends.wxss b/pages/social/friends/friends.wxss new file mode 100644 index 0000000..4b14964 --- /dev/null +++ b/pages/social/friends/friends.wxss @@ -0,0 +1,431 @@ +/* 好友页面 - 简洁现代设计 */ + +.friends-container { + height: 100vh; + background-color: #f5f5f5; + display: flex; + flex-direction: column; +} + +/* 导航栏 */ +.nav-bar { + background-color: #fff; + border-bottom: 1px solid #e5e5e5; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; +} + +.nav-content { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + height: 44px; +} + +.nav-left { + flex: 1; +} + +.nav-title { + font-size: 20px; + font-weight: 600; + color: #333; + line-height: 1.2; +} + +.nav-subtitle { + font-size: 12px; + color: #999; + margin-top: 2px; +} + +.nav-actions { + display: flex; + gap: 8px; +} + +.nav-btn { + width: 36px; + height: 36px; + border-radius: 18px; + background-color: #f0f0f0; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.nav-btn:active { + background-color: #e0e0e0; + transform: scale(0.95); +} + +.nav-icon { + font-size: 16px; +} + +/* 内容区域 */ +.content-area { + position: fixed; + left: 0; + right: 0; + bottom: 0; + background-color: #f5f5f5; +} + +/* 搜索栏 */ +.search-section { + padding: 12px 16px; + background-color: #fff; + border-bottom: 1px solid #e5e5e5; +} + +.search-bar { + position: relative; + background-color: #f0f0f0; + border-radius: 20px; + padding: 0 16px; +} + +.search-input { + height: 36px; + font-size: 14px; + color: #333; +} + +.search-clear { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + width: 20px; + height: 20px; + border-radius: 10px; + background-color: #ccc; + display: flex; + align-items: center; + justify-content: center; +} + +.clear-icon { + font-size: 12px; + color: #fff; +} + +/* 功能入口 */ +.function-section { + background-color: #fff; + margin-bottom: 12px; +} + +.function-item { + display: flex; + align-items: center; + padding: 16px; + border-bottom: 1px solid #f0f0f0; + transition: all 0.2s ease; +} + +.function-item:last-child { + border-bottom: none; +} + +.function-item:active { + background-color: #f8f8f8; +} + +.function-icon { + width: 40px; + height: 40px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 12px; + position: relative; +} + +.function-icon.new-friends { + background-color: #4CAF50; +} + +.function-icon.groups { + background-color: #2196F3; +} + +.icon-text { + font-size: 20px; + color: #fff; +} + +.badge { + position: absolute; + top: -4px; + right: -4px; + min-width: 18px; + height: 18px; + background-color: #ff4444; + border-radius: 9px; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + color: #fff; + font-weight: 600; + border: 2px solid #fff; +} + +.function-info { + flex: 1; +} + +.function-title { + font-size: 16px; + font-weight: 500; + color: #333; + margin-bottom: 2px; +} + +.function-desc { + font-size: 12px; + color: #999; +} + +.function-arrow { + font-size: 18px; + color: #ccc; +} + +/* 好友列表 */ +.friends-section { + background-color: #fff; +} + +.section-header { + padding: 12px 16px 8px 16px; + background-color: #f8f8f8; + border-bottom: 1px solid #e5e5e5; +} + +.section-title { + font-size: 14px; + color: #666; + font-weight: 500; +} + +.friends-list { + background-color: #fff; +} + +.friend-item { + display: flex; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid #f0f0f0; + transition: all 0.2s ease; +} + +.friend-item:last-child { + border-bottom: none; +} + +.friend-item:active { + background-color: #f8f8f8; +} + +/* 头像 */ +.friend-avatar { + width: 48px; + height: 48px; + border-radius: 24px; + margin-right: 12px; + position: relative; + overflow: hidden; +} + +.avatar-img { + width: 100%; + height: 100%; +} + +.avatar-placeholder { + width: 100%; + height: 100%; + background: linear-gradient(135deg, #667eea, #764ba2); + display: flex; + align-items: center; + justify-content: center; +} + +.avatar-text { + color: #fff; + font-size: 18px; + font-weight: 600; +} + +.online-dot { + position: absolute; + bottom: 2px; + right: 2px; + width: 12px; + height: 12px; + border-radius: 6px; + background-color: #ccc; + border: 2px solid #fff; +} + +.online-dot.online { + background-color: #4CAF50; +} + +/* 好友信息 */ +.friend-info { + flex: 1; + min-width: 0; +} + +.friend-name { + font-size: 16px; + font-weight: 500; + color: #333; + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.friend-status { + font-size: 12px; + color: #999; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* 操作按钮 */ +.friend-actions { + display: flex; + gap: 8px; +} + +.action-btn { + width: 32px; + height: 32px; + border-radius: 16px; + background-color: #f0f0f0; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.action-btn:active { + background-color: #e0e0e0; + transform: scale(0.9); +} + +.action-icon { + font-size: 14px; +} + +/* 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80px 32px; + text-align: center; +} + +.empty-icon { + font-size: 64px; + margin-bottom: 16px; + opacity: 0.3; +} + +.empty-title { + font-size: 18px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.empty-desc { + font-size: 14px; + color: #999; + margin-bottom: 24px; +} + +.empty-btn { + background: linear-gradient(135deg, #667eea, #764ba2); + color: #fff; + padding: 12px 24px; + border-radius: 20px; + transition: all 0.2s ease; +} + +.empty-btn:active { + transform: scale(0.95); +} + +.btn-text { + font-size: 14px; + font-weight: 500; + color: #fff; +} + +/* 加载状态 */ +.loading-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; +} + +.loading-spinner { + width: 32px; + height: 32px; + border: 3px solid #f0f0f0; + border-top: 3px solid #667eea; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 12px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-text { + font-size: 14px; + color: #999; +} + +/* 响应式设计 */ +@media (max-width: 375px) { + .nav-content { + padding: 8px 12px; + } + + .function-item, + .friend-item { + padding: 12px; + } + + .friend-avatar { + width: 44px; + height: 44px; + border-radius: 22px; + } + + .avatar-text { + font-size: 16px; + } +} \ No newline at end of file diff --git a/pages/social/search/search.js b/pages/social/search/search.js new file mode 100644 index 0000000..3f6f6aa --- /dev/null +++ b/pages/social/search/search.js @@ -0,0 +1,291 @@ +// 搜索用户页面 +const app = getApp(); +const friendAPI = require('../../../utils/friend-api.js'); + +Page({ + data: { + // 搜索相关 + searchKeyword: '', + searchResults: [], + loading: false, + hasSearched: false, + + // 搜索类型 + searchType: 'all', // all, nickname, custom_id, phone + searchTypes: [ + { value: 'all', label: '全部' }, + { value: 'nickname', label: '昵称' }, + { value: 'custom_id', label: 'ID' }, + { value: 'phone', label: '手机号' } + ], + + // 分页 + currentPage: 1, + pageSize: 20, + hasMore: true, + + // 系统信息 + statusBarHeight: 0, + navBarHeight: 0 + }, + + onLoad(options) { + console.log('搜索用户页面加载'); + this.initSystemInfo(); + + // 如果有传入的搜索关键词,直接搜索 + if (options.keyword) { + this.setData({ + searchKeyword: decodeURIComponent(options.keyword) + }); + this.performSearch(); + } + }, + + // 初始化系统信息 + initSystemInfo() { + try { + // 使用新的API替代已弃用的wx.getSystemInfoSync + const windowInfo = wx.getWindowInfo(); + const menuButton = wx.getMenuButtonBoundingClientRect(); + + this.setData({ + statusBarHeight: windowInfo.statusBarHeight, + navBarHeight: menuButton.bottom + 10 + }); + } catch (error) { + console.error('获取系统信息失败,使用兜底方案:', error); + // 兜底方案 + try { + const systemInfo = wx.getSystemInfoSync(); + const menuButton = wx.getMenuButtonBoundingClientRect(); + + this.setData({ + statusBarHeight: systemInfo.statusBarHeight, + navBarHeight: menuButton.bottom + 10 + }); + } catch (fallbackError) { + console.error('兜底方案也失败了:', fallbackError); + // 设置默认值 + this.setData({ + statusBarHeight: 44, + navBarHeight: 88 + }); + } + } + }, + + // 返回上一页 + goBack() { + wx.navigateBack(); + }, + + // 搜索输入 + onSearchInput(e) { + this.setData({ + searchKeyword: e.detail.value + }); + }, + + // 搜索确认 + onSearchConfirm() { + this.performSearch(); + }, + + // 清空搜索 + clearSearch() { + this.setData({ + searchKeyword: '', + searchResults: [], + hasSearched: false, + currentPage: 1, + hasMore: true + }); + }, + + // 切换搜索类型 + onSearchTypeChange(e) { + const searchType = e.currentTarget.dataset.type; + this.setData({ + searchType + }); + + if (this.data.searchKeyword) { + this.performSearch(); + } + }, + + // 执行搜索 + async performSearch() { + const keyword = this.data.searchKeyword.trim(); + if (!keyword) { + wx.showToast({ + title: '请输入搜索关键词', + icon: 'none' + }); + return; + } + + this.setData({ + loading: true, + currentPage: 1, + searchResults: [], + hasMore: true + }); + + try { + const response = await friendAPI.searchUsers( + keyword, + this.data.searchType, + 1, + this.data.pageSize + ); + + // 根据正确的接口文档处理API响应结构 + const searchData = response.data || {}; + const users = searchData.users || []; + const total = searchData.total || 0; + + this.setData({ + searchResults: users, + hasSearched: true, + hasMore: users.length >= this.data.pageSize && this.data.currentPage * this.data.pageSize < total, + loading: false + }); + + if (users.length === 0) { + wx.showToast({ + title: '未找到相关用户', + icon: 'none' + }); + } + + } catch (error) { + console.error('搜索失败:', error); + this.setData({ + loading: false, + hasSearched: true + }); + + wx.showToast({ + title: error.message || '搜索失败', + icon: 'none' + }); + } + }, + + // 加载更多 + async loadMore() { + if (!this.data.hasMore || this.data.loading) return; + + this.setData({ loading: true }); + + try { + const response = await friendAPI.searchUsers( + this.data.searchKeyword, + this.data.searchType, + this.data.currentPage + 1, + this.data.pageSize + ); + + const searchData = response.data || {}; + const newUsers = searchData.users || []; + const total = searchData.total || 0; + const newResults = [...this.data.searchResults, ...newUsers]; + + this.setData({ + searchResults: newResults, + currentPage: this.data.currentPage + 1, + hasMore: newResults.length < total, + loading: false + }); + + } catch (error) { + console.error('加载更多失败:', error); + this.setData({ loading: false }); + } + }, + + // 添加好友 + async addFriend(e) { + const { customId, nickname } = e.currentTarget.dataset; + + try { + // 显示输入框让用户输入验证消息 + const result = await this.showAddFriendDialog(nickname); + if (!result.confirm) return; + + wx.showLoading({ title: '发送中...' }); + + await friendAPI.addFriend(customId, result.message); + + wx.hideLoading(); + wx.showToast({ + title: '好友请求已发送', + icon: 'success' + }); + + // 更新按钮状态 + const updatedResults = this.data.searchResults.map(user => { + if (user.customId === customId) { + return { + ...user, + relationStatus: 'pending', + actionText: '等待验证', + canAddFriend: false + }; + } + return user; + }); + + this.setData({ + searchResults: updatedResults + }); + + } catch (error) { + wx.hideLoading(); + console.error('添加好友失败:', error); + wx.showToast({ + title: error.message || '添加好友失败', + icon: 'none' + }); + } + }, + + // 显示添加好友对话框 + showAddFriendDialog(nickname) { + return new Promise((resolve) => { + wx.showModal({ + title: `添加 ${nickname} 为好友`, + editable: true, + placeholderText: '请输入验证消息', + content: `我是 ${app.globalData.userInfo?.user?.nickname || ''}`, + success: (res) => { + resolve({ + confirm: res.confirm, + message: res.content || `我是 ${app.globalData.userInfo?.user?.nickname || ''}` + }); + }, + fail: () => { + resolve({ confirm: false }); + } + }); + }); + }, + + // 查看用户详情 + viewUserDetail(e) { + const { customId } = e.currentTarget.dataset; + wx.navigateTo({ + url: `/pages/social/user-detail/user-detail?customId=${customId}` + }); + }, + + // 发送消息 + sendMessage(e) { + const { customId, nickname } = e.currentTarget.dataset; + wx.navigateTo({ + url: `/pages/message/chat/chat?targetId=${customId}&name=${encodeURIComponent(nickname)}&chatType=0` + }); + } +}); diff --git a/pages/social/search/search.wxml b/pages/social/search/search.wxml new file mode 100644 index 0000000..5bf19e7 --- /dev/null +++ b/pages/social/search/search.wxml @@ -0,0 +1,168 @@ + + + + + + + + + + + 添加好友 + + + + + + + + + + + 🔍 + + + + + + + 搜索 + + + + + + + {{item.label}} + + + + + + + + + + + 搜索中... + + + + + 🔍 + 搜索用户 + 输入昵称、ID或手机号查找用户 + + • 支持模糊搜索昵称 + • 支持精确搜索用户ID + • 支持手机号搜索 + + + + + + 😔 + 未找到相关用户 + 试试其他关键词或搜索方式 + + + + + + + + + + + + + + {{item.actionText}} + + + + + 等待验证 + + + + + 已是好友 + + + + + 发消息 + + + + + + + + + {{loading ? '加载中...' : '上拉加载更多'}} + + + + + 没有更多用户了 + + + diff --git a/pages/social/search/search.wxss b/pages/social/search/search.wxss new file mode 100644 index 0000000..61653f3 --- /dev/null +++ b/pages/social/search/search.wxss @@ -0,0 +1,470 @@ +/* 搜索用户页面样式 */ + +.search-container { + height: 100vh; + background-color: #f8f9fa; + display: flex; + flex-direction: column; +} + +/* 自定义导航栏 */ +.custom-nav-bar { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; +} + +.nav-content { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32rpx; + height: 88rpx; +} + +.nav-left { + width: 80rpx; + display: flex; + align-items: center; +} + +.back-icon { + font-size: 40rpx; + color: white; + font-weight: bold; +} + +.nav-center { + flex: 1; + text-align: center; +} + +.nav-title { + font-size: 36rpx; + font-weight: 600; + color: white; +} + +.nav-right { + width: 80rpx; +} + +/* 搜索区域 */ +.search-section { + margin-top: 176rpx; + padding: 32rpx; + background: white; + border-bottom: 1px solid #f0f0f0; +} + +.search-box { + display: flex; + align-items: center; + gap: 24rpx; + margin-bottom: 32rpx; +} + +.search-input-wrapper { + flex: 1; + position: relative; + background: #f8f9fa; + border-radius: 50rpx; + padding: 0 32rpx; + display: flex; + align-items: center; + height: 88rpx; +} + +.search-icon { + font-size: 32rpx; + color: #999; + margin-right: 16rpx; +} + +.search-input { + flex: 1; + font-size: 32rpx; + color: #333; +} + +.clear-btn { + opacity: 0; + transition: opacity 0.3s; + padding: 8rpx; +} + +.clear-btn.show { + opacity: 1; +} + +.clear-icon { + font-size: 28rpx; + color: #999; +} + +.search-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 50rpx; + padding: 0 32rpx; + height: 88rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.search-btn-text { + color: white; + font-size: 32rpx; + font-weight: 600; +} + +/* 搜索类型 */ +.search-types { + display: flex; + gap: 16rpx; +} + +.type-item { + padding: 16rpx 32rpx; + border-radius: 50rpx; + background: #f8f9fa; + border: 2rpx solid transparent; + transition: all 0.3s; +} + +.type-item.active { + background: #e3f2fd; + border-color: #2196f3; +} + +.type-text { + font-size: 28rpx; + color: #666; +} + +.type-item.active .type-text { + color: #2196f3; + font-weight: 600; +} + +/* 结果容器 */ +.results-container { + flex: 1; + padding: 0 32rpx; +} + +/* 加载中 */ +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 120rpx 0; +} + +.loading-spinner { + width: 60rpx; + height: 60rpx; + border: 4rpx solid #f3f3f3; + border-top: 4rpx solid #667eea; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 24rpx; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-text { + font-size: 28rpx; + color: #999; +} + +/* 搜索提示 */ +.search-tips { + display: flex; + flex-direction: column; + align-items: center; + padding: 120rpx 0; + text-align: center; +} + +.tips-icon { + font-size: 120rpx; + margin-bottom: 32rpx; + opacity: 0.6; +} + +.tips-title { + font-size: 36rpx; + font-weight: 600; + color: #333; + margin-bottom: 16rpx; +} + +.tips-desc { + font-size: 28rpx; + color: #666; + margin-bottom: 48rpx; +} + +.tips-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.tip-item { + font-size: 26rpx; + color: #999; + text-align: left; +} + +/* 无结果 */ +.no-results { + display: flex; + flex-direction: column; + align-items: center; + padding: 120rpx 0; + text-align: center; +} + +.no-results-icon { + font-size: 120rpx; + margin-bottom: 32rpx; + opacity: 0.6; +} + +.no-results-title { + font-size: 36rpx; + font-weight: 600; + color: #333; + margin-bottom: 16rpx; +} + +.no-results-desc { + font-size: 28rpx; + color: #666; +} + +/* 结果列表 */ +.results-list { + padding: 32rpx 0; +} + +.result-item { + background: white; + border-radius: 24rpx; + padding: 32rpx; + margin-bottom: 24rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); + display: flex; + align-items: center; + justify-content: space-between; +} + +/* 用户信息 */ +.user-info { + display: flex; + align-items: center; + flex: 1; + margin-right: 24rpx; +} + +.user-avatar { + position: relative; + margin-right: 24rpx; +} + +.avatar-image { + width: 96rpx; + height: 96rpx; + border-radius: 48rpx; +} + +.avatar-placeholder { + width: 96rpx; + height: 96rpx; + border-radius: 48rpx; + background: #e0e0e0; + display: flex; + align-items: center; + justify-content: center; +} + +.avatar-text { + font-size: 36rpx; + font-weight: 600; + color: #666; +} + +.member-badge { + position: absolute; + bottom: -4rpx; + right: -4rpx; + background: linear-gradient(135deg, #ffd700, #ffb300); + border-radius: 20rpx; + padding: 4rpx 8rpx; +} + +.member-text { + font-size: 20rpx; + color: white; + font-weight: 600; +} + +.user-details { + flex: 1; +} + +.user-name-row { + display: flex; + align-items: center; + margin-bottom: 8rpx; +} + +.user-nickname { + font-size: 32rpx; + font-weight: 600; + color: #333; + margin-right: 16rpx; +} + +.gender-icon { + width: 32rpx; + height: 32rpx; + border-radius: 16rpx; + display: flex; + align-items: center; + justify-content: center; + background: #2196f3; +} + +.gender-text { + font-size: 20rpx; + color: white; + font-weight: 600; +} + +.user-meta { + display: flex; + align-items: center; + gap: 16rpx; + margin-bottom: 8rpx; +} + +.user-id { + font-size: 24rpx; + color: #999; +} + +.match-type { + font-size: 24rpx; + color: #2196f3; + background: #e3f2fd; + padding: 4rpx 12rpx; + border-radius: 12rpx; +} + +.user-bio { + font-size: 26rpx; + color: #666; + margin-bottom: 8rpx; +} + +.status-message { + font-size: 24rpx; + color: #999; +} + +/* 操作按钮 */ +.action-buttons { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.action-btn { + padding: 16rpx 32rpx; + border-radius: 50rpx; + text-align: center; + min-width: 120rpx; +} + +.add-btn.enabled { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.add-btn.disabled { + background: #f0f0f0; +} + +.pending-btn { + background: #fff3e0; + border: 2rpx solid #ff9800; +} + +.friend-btn { + background: #e8f5e8; + border: 2rpx solid #4caf50; +} + +.message-btn { + background: #e3f2fd; + border: 2rpx solid #2196f3; +} + +.btn-text { + font-size: 26rpx; + font-weight: 600; +} + +.add-btn.enabled .btn-text { + color: white; +} + +.add-btn.disabled .btn-text { + color: #999; +} + +.pending-btn .btn-text { + color: #ff9800; +} + +.friend-btn .btn-text { + color: #4caf50; +} + +.message-btn .btn-text { + color: #2196f3; +} + +/* 加载更多 */ +.load-more { + display: flex; + align-items: center; + justify-content: center; + padding: 32rpx 0; + gap: 16rpx; +} + +.load-more-text { + font-size: 28rpx; + color: #999; +} + +.no-more { + display: flex; + justify-content: center; + padding: 32rpx 0; +} + +.no-more-text { + font-size: 28rpx; + color: #ccc; +} diff --git a/pages/splash/splash.js b/pages/splash/splash.js new file mode 100644 index 0000000..cb4aee9 --- /dev/null +++ b/pages/splash/splash.js @@ -0,0 +1,283 @@ +// 启动页逻辑 - 性能优化版本 +const app = getApp(); +const config = require('../../config/config.js'); +const systemInfoUtil = require('../../utils/system-info.js'); +const wsManager = require('../../utils/websocket-manager-v2.js'); + +Page({ + data: { + // 应用信息 + appName: config.appName || 'FindMe', + appVersion: config.appVersion || '1.0.0', + + // 状态控制 + isLoading: true, + showProgress: false, + showError: false, + + // 显示文本 + loadingText: '正在启动...', + errorMessage: '', + progress: 0, + + // 系统适配信息 - 先设置默认值 + statusBarHeight: 44, + menuButtonHeight: 32, + menuButtonTop: 6, + navBarHeight: 88, + windowHeight: 667, + safeAreaBottom: 0 + }, + + // 页面加载 - 性能优化 + onLoad: function (options) { + console.log('启动页加载开始'); + const startTime = Date.now(); + + // 立即开始启动流程,系统信息异步初始化 + this.startLaunchSequence().then(() => { + const endTime = Date.now(); + console.log(`启动页加载完成,耗时: ${endTime - startTime}ms`); + }); + + // 异步初始化系统信息,不阻塞启动流程 + this.initSystemInfoAsync(); + }, + + // 页面显示 + onShow: function () { + console.log('启动页显示'); + }, + + // 页面隐藏 + onHide: function () { + console.log('启动页隐藏'); + }, + + // 异步初始化系统信息 + async initSystemInfoAsync() { + try { + await systemInfoUtil.init(); + const adaptInfo = systemInfoUtil.getSystemAdaptInfo(); + + this.setData({ + statusBarHeight: adaptInfo.statusBarHeight, + menuButtonHeight: adaptInfo.menuButtonHeight, + menuButtonTop: adaptInfo.menuButtonTop, + navBarHeight: adaptInfo.navBarHeight, + windowHeight: adaptInfo.windowHeight, + safeAreaBottom: adaptInfo.safeAreaBottom + }); + + console.log('系统信息异步初始化完成'); + } catch (error) { + console.error('系统信息初始化失败,使用默认值:', error); + } + }, + + // 开始启动流程 - 优化版本 + async startLaunchSequence() { + try { + console.log('开始应用启动流程'); + + // 第1步:显示启动动画 (0-20%) + this.updateProgress(20, '初始化应用...'); + await this.delay(200); // 减少延迟时间 + + // 第2步:检查基础服务 (20-50%) + await this.updateProgress(50, '检查服务状态...'); + await this.checkBasicServices(); + + // 第3步:初始化用户数据 (50-80%) + await this.updateProgress(80, '加载用户数据...'); + await this.initUserData(); + + // 第4步:准备界面 (80-100%) + await this.updateProgress(100, '启动完成'); + await this.delay(300); // 减少完成状态显示时间 + + // 跳转到主页面 + this.navigateToMainPage(); + + } catch (error) { + console.error('启动失败:', error); + this.showError(error.message || '启动失败,请重试'); + } + }, + + // 检查基础服务 - 优化版本 + async checkBasicServices() { + try { + // 并行检查多个服务,提高效率 + const checks = [ + this.checkAppPermissions(), + this.checkNetworkStatus(), + this.delay(100) // 最小延迟,确保用户能看到进度 + ]; + + await Promise.all(checks); + console.log('基础服务检查完成'); + } catch (error) { + console.error('基础服务检查失败:', error); + throw new Error('服务检查失败'); + } + }, + + // 检查应用权限 - 快速版本 + async checkAppPermissions() { + return new Promise((resolve) => { + // 简化权限检查,避免耗时操作 + wx.getSetting({ + success: () => { + console.log('权限检查完成'); + resolve(); + }, + fail: () => { + console.log('权限检查失败,继续启动'); + resolve(); // 不阻塞启动流程 + } + }); + }); + }, + + // 检查网络状态 - 快速版本 + async checkNetworkStatus() { + return new Promise((resolve) => { + wx.getNetworkType({ + success: (res) => { + console.log('网络状态:', res.networkType); + resolve(); + }, + fail: () => { + console.log('网络检查失败,继续启动'); + resolve(); // 不阻塞启动流程 + } + }); + }); + }, + + // 初始化用户数据 - 优化版本 + async initUserData() { + try { + // 检查本地存储的用户信息 + const userInfo = wx.getStorageSync('userInfo'); + if (userInfo) { + console.log('发现本地用户信息'); + app.globalData.userInfo = userInfo; + app.globalData.isLoggedIn = true; + + // 如果用户已登录,测试WebSocket连接 + this.testWebSocketConnection(); + } + + // 异步更新用户数据,不阻塞启动 + this.updateUserDataAsync(); + + } catch (error) { + console.error('用户数据初始化失败:', error); + // 不抛出错误,允许继续启动 + } + }, + + // 测试WebSocket连接 - 异步执行,不阻塞启动 + async testWebSocketConnection() { + try { + console.log('🔌 启动页面:开始测试WebSocket连接...'); + + // 设置token到WebSocket管理器 + const userInfo = app.globalData.userInfo; + if (userInfo && userInfo.token) { + wsManager.setToken(userInfo.token); + + // 异步尝试连接,不等待结果 + wsManager.connect().then(() => { + console.log('✅ 启动页面:WebSocket连接成功'); + }).catch((error) => { + console.log('⚠️ 启动页面:WebSocket连接失败,稍后重试:', error.message); + }); + } + } catch (error) { + console.log('⚠️ 启动页面:WebSocket连接测试失败:', error); + } + }, + + // 异步更新用户数据 + async updateUserDataAsync() { + try { + // 这里可以添加后台数据同步逻辑 + console.log('异步更新用户数据...'); + } catch (error) { + console.error('异步更新用户数据失败:', error); + } + }, + + // 更新进度 - 优化版本 + async updateProgress(progress, text) { + this.setData({ + progress: progress, + loadingText: text, + showProgress: true + }); + + // 使用requestAnimationFrame优化动画性能 + if (wx.nextTick) { + await new Promise(resolve => wx.nextTick(resolve)); + } + }, + + // 延迟函数 - 性能优化 + delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + }, + + // 跳转到主页面 - 优化版本 + navigateToMainPage() { + // 检查用户登录状态 + const userInfo = app.globalData.userInfo; + const targetPage = userInfo && userInfo.token ? '/pages/map/map' : '/pages/login/login'; + + console.log('跳转到:', targetPage); + + wx.reLaunch({ + url: targetPage, + success: () => { + console.log('页面跳转成功'); + }, + fail: (error) => { + console.error('页面跳转失败:', error); + // 兜底跳转到地图页面 + wx.reLaunch({ + url: '/pages/map/map' + }); + } + }); + }, + + // 显示错误 + showError(message) { + this.setData({ + showError: true, + errorMessage: message, + isLoading: false + }); + }, + + // 重试启动 + onRetry() { + this.setData({ + showError: false, + isLoading: true, + progress: 0 + }); + + // 延迟重试,避免立即重试 + setTimeout(() => { + this.startLaunchSequence(); + }, 300); + }, + + // 页面卸载 + onUnload() { + console.log('启动页卸载'); + } +}); \ No newline at end of file diff --git a/pages/splash/splash.json b/pages/splash/splash.json new file mode 100644 index 0000000..8b8b975 --- /dev/null +++ b/pages/splash/splash.json @@ -0,0 +1,7 @@ +{ + "navigationBarTitleText": "FindMe", + "navigationBarBackgroundColor": "#667eea", + "navigationBarTextStyle": "white", + "backgroundColor": "#667eea", + "disableScroll": true +} \ No newline at end of file diff --git a/pages/splash/splash.wxml b/pages/splash/splash.wxml new file mode 100644 index 0000000..d0b7a2d --- /dev/null +++ b/pages/splash/splash.wxml @@ -0,0 +1,92 @@ + + + + + + 🫐 + + 🌟 + 💫 + + 🫐 + + 🌟 + + + + + + + 🫐 + + + + + + + + + + + + {{appName}} + 发现身边,连接世界 + + + + + + + + + + + + + + + + {{loadingText}} + + + + + + + + + + {{progress}}% + 正在加载资源... + + + + + + + 🏷️ + v{{appVersion}} + + + + + + + ⚠️ + 启动失败 + {{errorMessage}} + + + 🔄 + 重试 + + + + + + + + + 启动成功 + + \ No newline at end of file diff --git a/pages/splash/splash.wxss b/pages/splash/splash.wxss new file mode 100644 index 0000000..568d425 --- /dev/null +++ b/pages/splash/splash.wxss @@ -0,0 +1,629 @@ +/* 启动页面 - 现代化设计 + 系统适配 */ +page { + margin: 0; + padding: 0; + box-sizing: border-box; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +/* 主容器 */ +.splash-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +/* 背景装饰 */ +.background-decorations { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + overflow: hidden; +} + +.decoration { + position: absolute; + font-size: 40rpx; + opacity: 0.1; + animation: float-decoration 8s ease-in-out infinite; +} + +.decoration-1 { top: 10%; left: 15%; animation-delay: 0s; } +.decoration-2 { top: 20%; right: 20%; animation-delay: -1s; } +.decoration-3 { top: 40%; left: 10%; animation-delay: -2s; } +.decoration-4 { top: 60%; right: 15%; animation-delay: -3s; } +.decoration-5 { top: 75%; left: 25%; animation-delay: -4s; } +.decoration-6 { top: 30%; right: 10%; animation-delay: -5s; } +.decoration-7 { top: 80%; right: 30%; animation-delay: -6s; } +.decoration-8 { top: 50%; left: 5%; animation-delay: -7s; } + +@keyframes float-decoration { + 0%, 100% { + transform: translateY(0) rotate(0deg) scale(1); + opacity: 0.1; + } + 25% { + transform: translateY(-20rpx) rotate(90deg) scale(1.1); + opacity: 0.2; + } + 50% { + transform: translateY(-40rpx) rotate(180deg) scale(0.9); + opacity: 0.3; + } + 75% { + transform: translateY(-20rpx) rotate(270deg) scale(1.1); + opacity: 0.2; + } +} + +/* Logo区域 */ +.logo-container { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 120rpx; + z-index: 10; +} + +.logo-animation { + position: relative; + width: 200rpx; + height: 200rpx; + margin-bottom: 60rpx; +} + +.logo-main { + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + z-index: 3; +} + +.logo-icon { + font-size: 100rpx; + text-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.3); + animation: logo-pulse 2s ease-in-out infinite; +} + +.logo-rings { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.ring { + position: absolute; + border: 2rpx solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + animation: ring-spin 6s linear infinite; +} + +.ring-1 { + width: 120rpx; + height: 120rpx; + top: 40rpx; + left: 40rpx; + animation-delay: 0s; +} + +.ring-2 { + width: 140rpx; + height: 140rpx; + top: 30rpx; + left: 30rpx; + animation-delay: -1s; + animation-direction: reverse; +} + +.ring-3 { + width: 160rpx; + height: 160rpx; + top: 20rpx; + left: 20rpx; + animation-delay: -2s; +} + +.ring-4 { + width: 180rpx; + height: 180rpx; + top: 10rpx; + left: 10rpx; + animation-delay: -3s; + animation-direction: reverse; +} + +.logo-glow { + position: absolute; + top: -20rpx; + left: -20rpx; + right: -20rpx; + bottom: -20rpx; + background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%); + border-radius: 50%; + animation: glow-pulse 3s ease-in-out infinite; +} + +@keyframes logo-pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +@keyframes ring-spin { + 0% { transform: rotate(0deg); opacity: 0.3; } + 50% { opacity: 0.6; } + 100% { transform: rotate(360deg); opacity: 0.3; } +} + +@keyframes glow-pulse { + 0%, 100% { opacity: 0.5; transform: scale(1); } + 50% { opacity: 0.8; transform: scale(1.1); } +} + +/* 应用信息 */ +.app-info { + display: flex; + flex-direction: column; + align-items: center; + gap: 16rpx; +} + +.app-name { + font-size: 56rpx; + font-weight: 700; + color: white; + text-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.3); + letter-spacing: 3rpx; + animation: fade-in-up 1s ease-out 0.5s both; +} + +.app-slogan { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.9); + text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2); + font-style: italic; + animation: fade-in-up 1s ease-out 0.8s both; +} + +@keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(30rpx); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 加载动画 */ +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 40rpx; + margin-bottom: 80rpx; + z-index: 10; +} + +.loading-animation { + position: relative; + width: 120rpx; + height: 120rpx; +} + +.spinner-container { + position: relative; + width: 100%; + height: 100%; +} + +.spinner-dot { + position: absolute; + width: 16rpx; + height: 16rpx; + background: rgba(255, 255, 255, 0.8); + border-radius: 50%; + animation: spinner-rotate 2s linear infinite; +} + +.dot-1 { + top: 10rpx; + left: 50%; + transform: translateX(-50%); + animation-delay: 0s; +} + +.dot-2 { + top: 30rpx; + right: 20rpx; + animation-delay: 0.2s; +} + +.dot-3 { + bottom: 20rpx; + right: 30rpx; + animation-delay: 0.4s; +} + +.dot-4 { + bottom: 10rpx; + left: 30rpx; + animation-delay: 0.6s; +} + +.dot-5 { + top: 30rpx; + left: 20rpx; + animation-delay: 0.8s; +} + +.pulse-ring { + position: absolute; + top: 50%; + left: 50%; + width: 80rpx; + height: 80rpx; + border: 3rpx solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + transform: translate(-50%, -50%); + animation: pulse-ring 2s ease-in-out infinite; +} + +@keyframes spinner-rotate { + 0% { + transform: rotate(0deg) translateX(40rpx) rotate(0deg); + opacity: 1; + } + 50% { + opacity: 0.3; + } + 100% { + transform: rotate(360deg) translateX(40rpx) rotate(-360deg); + opacity: 1; + } +} + +@keyframes pulse-ring { + 0% { + transform: translate(-50%, -50%) scale(0.8); + opacity: 1; + } + 50% { + transform: translate(-50%, -50%) scale(1.2); + opacity: 0.3; + } + 100% { + transform: translate(-50%, -50%) scale(0.8); + opacity: 1; + } +} + +.loading-text { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.9); + text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2); + animation: text-fade 2s ease-in-out infinite; +} + +@keyframes text-fade { + 0%, 100% { opacity: 0.7; } + 50% { opacity: 1; } +} + +/* 进度条 */ +.progress-container { + width: 500rpx; + margin-bottom: 60rpx; + z-index: 10; +} + +.progress-track { + position: relative; + width: 100%; + height: 8rpx; + background: rgba(255, 255, 255, 0.2); + border-radius: 4rpx; + overflow: hidden; + backdrop-filter: blur(10rpx); +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, rgba(255, 255, 255, 0.8) 0%, white 100%); + border-radius: 4rpx; + transition: width 0.3s ease; + box-shadow: 0 0 10rpx rgba(255, 255, 255, 0.5); +} + +.progress-glow { + position: absolute; + top: -4rpx; + width: 20rpx; + height: 16rpx; + background: radial-gradient(circle, rgba(255, 255, 255, 0.8) 0%, transparent 70%); + border-radius: 50%; + transform: translateX(-50%); + transition: left 0.3s ease; +} + +.progress-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 16rpx; +} + +.progress-percentage { + font-size: 24rpx; + font-weight: 600; + color: white; + text-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.2); +} + +.progress-description { + font-size: 22rpx; + color: rgba(255, 255, 255, 0.8); + text-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.2); +} + +/* 版本信息 */ +.version-info { + position: absolute; + left: 50%; + transform: translateX(-50%); + z-index: 10; +} + +.version-badge { + display: flex; + align-items: center; + gap: 12rpx; + padding: 12rpx 24rpx; + background: rgba(255, 255, 255, 0.1); + border-radius: 32rpx; + backdrop-filter: blur(20rpx); + border: 1rpx solid rgba(255, 255, 255, 0.2); +} + +.version-icon { + font-size: 20rpx; +} + +.version-text { + font-size: 22rpx; + color: rgba(255, 255, 255, 0.8); + font-family: 'Monaco', 'Consolas', monospace; +} + +/* 错误提示 */ +.error-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + backdrop-filter: blur(8rpx); + animation: fade-in 0.3s ease; +} + +.error-content { + background: rgba(255, 255, 255, 0.95); + border-radius: 32rpx; + padding: 60rpx 48rpx; + margin: 60rpx; + max-width: 500rpx; + text-align: center; + backdrop-filter: blur(20rpx); + box-shadow: 0 16rpx 64rpx rgba(0, 0, 0, 0.2); + animation: scale-in 0.3s ease; +} + +.error-icon { + font-size: 80rpx; + margin-bottom: 32rpx; + animation: shake 0.5s ease-in-out; +} + +.error-title { + font-size: 32rpx; + font-weight: 700; + color: #e74c3c; + margin-bottom: 16rpx; + display: block; +} + +.error-message { + font-size: 26rpx; + color: #6c757d; + line-height: 1.4; + margin-bottom: 40rpx; + display: block; +} + +.error-actions { + display: flex; + justify-content: center; +} + +.retry-button { + display: flex; + align-items: center; + gap: 16rpx; + padding: 20rpx 40rpx; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 24rpx; + box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3); + transition: all 0.2s ease; +} + +.retry-button:active { + transform: scale(0.95); + box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3); +} + +.button-icon { + font-size: 24rpx; + color: white; +} + +.button-text { + font-size: 28rpx; + font-weight: 600; + color: white; +} + +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes scale-in { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-10rpx); } + 75% { transform: translateX(10rpx); } +} + +/* 启动完成动画 */ +.success-animation { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 24rpx; + z-index: 100; + animation: success-appear 0.5s ease; +} + +.success-icon { + font-size: 100rpx; + animation: success-bounce 0.6s ease; +} + +.success-text { + font-size: 32rpx; + font-weight: 600; + color: white; + text-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.3); +} + +@keyframes success-appear { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.5); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + +@keyframes success-bounce { + 0% { transform: scale(0); } + 50% { transform: scale(1.2); } + 100% { transform: scale(1); } +} + +/* 响应式设计 - 适配不同屏幕尺寸 */ +@media screen and (max-width: 375px) { + .logo-animation { + width: 160rpx; + height: 160rpx; + margin-bottom: 40rpx; + } + + .logo-icon { + font-size: 80rpx; + } + + .app-name { + font-size: 48rpx; + } + + .app-slogan { + font-size: 24rpx; + } + + .progress-container { + width: 280rpx; + } + + .error-content { + padding: 48rpx 32rpx; + margin: 40rpx; + } +} + +@media screen and (min-width: 414px) { + .logo-animation { + width: 240rpx; + height: 240rpx; + margin-bottom: 80rpx; + } + + .logo-icon { + font-size: 120rpx; + } + + .app-name { + font-size: 64rpx; + } + + .app-slogan { + font-size: 32rpx; + } + + .progress-container { + width: 400rpx; + } +} + +@media screen and (max-height: 667px) { + .logo-container { + margin-bottom: 80rpx; + } + + .progress-container { + margin-bottom: 60rpx; + } +} + +@media screen and (min-height: 812px) { + .logo-container { + margin-bottom: 160rpx; + } + + .progress-container { + margin-bottom: 120rpx; + } +} \ No newline at end of file diff --git a/pages/websocket-test/websocket-test.js b/pages/websocket-test/websocket-test.js new file mode 100644 index 0000000..20ab318 --- /dev/null +++ b/pages/websocket-test/websocket-test.js @@ -0,0 +1,317 @@ +// WebSocket连接测试页面 +const app = getApp(); +const wsManager = require('../../utils/websocket-manager-v2.js'); + +Page({ + data: { + // 连接状态 + connectionStatus: '未连接', + isConnected: false, + isConnecting: false, + + // 测试信息 + testResults: [], + logs: [], + + // 用户信息 + userInfo: null, + hasToken: false, + + // 测试消息 + testMessage: 'Hello WebSocket!', + + // 统计信息 + messagesSent: 0, + messagesReceived: 0, + connectionAttempts: 0, + lastConnectTime: null, + lastDisconnectTime: null + }, + + // 页面加载 + onLoad() { + console.log('WebSocket测试页面加载'); + + // 初始化WebSocket管理器 + this.initWebSocket(); + + // 注册消息监听器 + this.registerMessageHandlers(); + }, + + // 注册消息处理器 + registerMessageHandlers() { + // 监听所有类型的消息 + wsManager.on('new_message', (message) => { + this.addLog(`📨 收到new_message: ${JSON.stringify(message)}`); + this.setData({ + messagesReceived: this.data.messagesReceived + 1 + }); + }); + + wsManager.on('message_status', (message) => { + this.addLog(`📨 收到message_status: ${JSON.stringify(message)}`); + this.setData({ + messagesReceived: this.data.messagesReceived + 1 + }); + }); + + wsManager.on('unread_count_update', (message) => { + this.addLog(`📨 收到unread_count_update: ${JSON.stringify(message)}`); + this.setData({ + messagesReceived: this.data.messagesReceived + 1 + }); + }); + + wsManager.on('message', (message) => { + this.addLog(`📨 收到通用message: ${JSON.stringify(message)}`); + this.setData({ + messagesReceived: this.data.messagesReceived + 1 + }); + }); + + // 连接状态监听 + wsManager.on('connected', () => { + this.addLog('✅ WebSocket连接成功'); + this.setData({ + isConnected: true, + connectionStatus: '已连接', + lastConnectTime: new Date().toLocaleTimeString() + }); + }); + + wsManager.on('disconnected', () => { + this.addLog('❌ WebSocket连接断开'); + this.setData({ + isConnected: false, + connectionStatus: '已断开' + }); + }); + + wsManager.on('error', (error) => { + this.addLog(`❌ WebSocket错误: ${error}`); + }); + }, + + // 更新连接状态 + updateStatus() { + const status = wsManager.getStatus(); + + let statusText = '未连接'; + if (status.isConnecting) { + statusText = '连接中...'; + } else if (status.isConnected) { + statusText = '已连接'; + } else if (status.reconnectAttempts > 0) { + statusText = `重连中 (${status.reconnectAttempts})`; + } + + this.setData({ + connectionStatus: statusText, + isConnected: status.isConnected, + isConnecting: status.isConnecting + }); + }, + + // 开始状态监控 + startStatusMonitoring() { + this.statusTimer = setInterval(() => { + this.updateStatus(); + }, 1000); + }, + + // 停止状态监控 + stopStatusMonitoring() { + if (this.statusTimer) { + clearInterval(this.statusTimer); + this.statusTimer = null; + } + }, + + // 添加日志 + addLog(message) { + const timestamp = new Date().toLocaleTimeString(); + const logEntry = `[${timestamp}] ${message}`; + + this.setData({ + logs: [logEntry, ...this.data.logs.slice(0, 49)] // 保留最近50条日志 + }); + + console.log(logEntry); + }, + + // 连接WebSocket + async onConnect() { + try { + this.addLog('🚀 开始连接WebSocket...'); + this.setData({ + connectionAttempts: this.data.connectionAttempts + 1 + }); + + await wsManager.connect(); + this.addLog('✅ 连接请求已发送'); + } catch (error) { + this.addLog(`❌ 连接失败: ${error.message}`); + } + }, + + // 断开WebSocket + onDisconnect() { + this.addLog('🔌 主动断开连接...'); + wsManager.disconnect(); + }, + + // 发送测试消息 + onSendMessage() { + if (!this.data.isConnected) { + wx.showToast({ + title: 'WebSocket未连接', + icon: 'none' + }); + return; + } + + const message = { + type: 'test', + content: this.data.testMessage, + timestamp: Date.now() + }; + + const success = wsManager.send(message); + if (success) { + this.addLog(`📤 发送消息: ${this.data.testMessage}`); + this.setData({ + messagesSent: this.data.messagesSent + 1 + }); + } else { + this.addLog('❌ 消息发送失败'); + } + }, + + // 🔥 测试发送正确格式的聊天消息 + onSendChatMessage() { + if (!this.data.isConnected) { + wx.showToast({ + title: 'WebSocket未连接', + icon: 'none' + }); + return; + } + + // 🔥 详细记录发送的消息格式 + const messageDetails = { + receiverId: '9118366451', + content: this.data.testMessage, + msgType: 'text', + chatType: 0 + }; + + console.log('🔍 准备发送聊天消息:', messageDetails); + + const success = wsManager.sendChatMessage( + messageDetails.receiverId, + messageDetails.content, + messageDetails.msgType, + messageDetails.chatType + ); + + if (success) { + this.addLog(`📤 发送聊天消息: ${this.data.testMessage}`); + this.addLog(`📋 消息详情: ${JSON.stringify(messageDetails)}`); + this.setData({ + messagesSent: this.data.messagesSent + 1 + }); + } else { + this.addLog('❌ 聊天消息发送失败'); + } + }, + + // 🔥 测试发送心跳消息 + onSendHeartbeat() { + if (!this.data.isConnected) { + wx.showToast({ + title: 'WebSocket未连接', + icon: 'none' + }); + return; + } + + const heartbeatMessage = { + type: 'heartbeat', + id: `heartbeat_${Date.now()}`, + data: { + timestamp: Date.now() + } + }; + + const success = wsManager.send(heartbeatMessage); + if (success) { + this.addLog('💓 发送心跳消息'); + this.setData({ + messagesSent: this.data.messagesSent + 1 + }); + } else { + this.addLog('❌ 心跳消息发送失败'); + } + }, + + // 清空日志 + onClearLogs() { + this.setData({ + logs: [], + messagesSent: 0, + messagesReceived: 0, + connectionAttempts: 0 + }); + this.addLog('📝 日志已清空'); + }, + + // 输入框变化 + onInputChange(e) { + this.setData({ + testMessage: e.detail.value + }); + }, + + // 复制日志 + onCopyLogs() { + const logsText = this.data.logs.join('\n'); + wx.setClipboardData({ + data: logsText, + success: () => { + wx.showToast({ + title: '日志已复制', + icon: 'success' + }); + } + }); + }, + + // 查看连接详情 + onShowDetails() { + const status = wsManager.getStatus(); + const details = [ + `连接状态: ${status.isConnected ? '已连接' : '未连接'}`, + `连接中: ${status.isConnecting ? '是' : '否'}`, + `重连次数: ${status.reconnectAttempts}`, + `有Token: ${status.hasToken ? '是' : '否'}`, + `设备ID: ${status.deviceId}`, + `发送消息数: ${this.data.messagesSent}`, + `接收消息数: ${this.data.messagesReceived}`, + `连接尝试数: ${this.data.connectionAttempts}`, + `最后连接时间: ${this.data.lastConnectTime || '无'}`, + `最后断开时间: ${this.data.lastDisconnectTime || '无'}` + ].join('\n'); + + wx.showModal({ + title: 'WebSocket连接详情', + content: details, + showCancel: false + }); + }, + + // 返回上一页 + onBack() { + wx.navigateBack(); + } +}); diff --git a/pages/websocket-test/websocket-test.json b/pages/websocket-test/websocket-test.json new file mode 100644 index 0000000..58db183 --- /dev/null +++ b/pages/websocket-test/websocket-test.json @@ -0,0 +1,8 @@ +{ + "navigationBarTitleText": "WebSocket测试", + "navigationBarBackgroundColor": "#667eea", + "navigationBarTextStyle": "white", + "backgroundColor": "#f5f5f5", + "enablePullDownRefresh": false, + "disableScroll": false +} diff --git a/pages/websocket-test/websocket-test.wxml b/pages/websocket-test/websocket-test.wxml new file mode 100644 index 0000000..b9b956a --- /dev/null +++ b/pages/websocket-test/websocket-test.wxml @@ -0,0 +1,110 @@ + + + + + + + + WebSocket测试 + + + + + + + + + + {{connectionStatus}} + + Token: {{hasToken ? '✓' : '✗'}} + 发送: {{messagesSent}} + 接收: {{messagesReceived}} + + + + + + + + + + + + + + + + + + + + 发送测试消息 + + + + + + + + + + + + + + + 连接日志 + 复制 + + + + + {{item}} + + + 暂无日志 + + + + + + + 统计信息 + + + {{connectionAttempts}} + 连接尝试 + + + {{messagesSent}} + 发送消息 + + + {{messagesReceived}} + 接收消息 + + + {{lastConnectTime || '-'}} + 最后连接 + + + + diff --git a/pages/websocket-test/websocket-test.wxss b/pages/websocket-test/websocket-test.wxss new file mode 100644 index 0000000..c57dcee --- /dev/null +++ b/pages/websocket-test/websocket-test.wxss @@ -0,0 +1,251 @@ +/* WebSocket测试页面样式 */ +.container { + min-height: 100vh; + background-color: #f5f5f5; + padding-bottom: 20rpx; +} + +/* 标题栏 */ +.header { + display: flex; + align-items: center; + justify-content: space-between; + height: 88rpx; + background-color: #667eea; + color: white; + padding: 0 30rpx; + position: sticky; + top: 0; + z-index: 100; +} + +.header-left, .header-right { + width: 60rpx; + height: 60rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background-color: rgba(255, 255, 255, 0.1); +} + +.header-title { + font-size: 36rpx; + font-weight: 600; +} + +.back-icon, .detail-icon { + font-size: 32rpx; +} + +/* 状态区域 */ +.status-section { + padding: 30rpx; +} + +.status-card { + background-color: white; + border-radius: 20rpx; + padding: 40rpx; + display: flex; + flex-direction: column; + align-items: center; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); +} + +.status-indicator { + width: 40rpx; + height: 40rpx; + border-radius: 50%; + margin-bottom: 20rpx; +} + +.status-indicator.connected { + background-color: #52c41a; + box-shadow: 0 0 20rpx rgba(82, 196, 26, 0.5); +} + +.status-indicator.disconnected { + background-color: #ff4d4f; + box-shadow: 0 0 20rpx rgba(255, 77, 79, 0.5); +} + +.status-text { + font-size: 36rpx; + font-weight: 600; + color: #333; + margin-bottom: 20rpx; +} + +.status-info { + display: flex; + gap: 30rpx; +} + +.info-item { + font-size: 24rpx; + color: #666; + background-color: #f0f0f0; + padding: 8rpx 16rpx; + border-radius: 12rpx; +} + +/* 操作区域 */ +.action-section { + padding: 0 30rpx 30rpx; +} + +.button-row { + display: flex; + gap: 20rpx; + margin-bottom: 20rpx; +} + +.action-btn { + flex: 1; + height: 80rpx; + border-radius: 16rpx; + font-size: 28rpx; + font-weight: 600; + border: none; + color: white; +} + +.connect-btn { + background: linear-gradient(135deg, #52c41a, #73d13d); +} + +.disconnect-btn { + background: linear-gradient(135deg, #ff4d4f, #ff7875); +} + +.heartbeat-btn { + background: linear-gradient(135deg, #1890ff, #40a9ff); +} + +.clear-btn { + background: linear-gradient(135deg, #faad14, #ffc53d); +} + +.action-btn:disabled { + background: #d9d9d9 !important; + color: #999 !important; +} + +/* 消息区域 */ +.message-section { + padding: 0 30rpx 30rpx; +} + +.section-title { + font-size: 32rpx; + font-weight: 600; + color: #333; + margin-bottom: 20rpx; +} + +.input-row { + display: flex; + gap: 20rpx; + align-items: center; +} + +.message-input { + flex: 1; + height: 80rpx; + background-color: white; + border-radius: 16rpx; + padding: 0 24rpx; + font-size: 28rpx; + border: 2rpx solid #e8e8e8; +} + +.send-btn { + width: 120rpx; + height: 80rpx; + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + border-radius: 16rpx; + font-size: 28rpx; + font-weight: 600; + border: none; +} + +.send-btn:disabled { + background: #d9d9d9 !important; + color: #999 !important; +} + +/* 日志区域 */ +.log-section { + padding: 0 30rpx 30rpx; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20rpx; +} + +.copy-btn { + font-size: 24rpx; + color: #667eea; + padding: 8rpx 16rpx; + background-color: rgba(102, 126, 234, 0.1); + border-radius: 12rpx; +} + +.log-container { + height: 400rpx; + background-color: #1e1e1e; + border-radius: 16rpx; + padding: 20rpx; +} + +.log-item { + font-size: 24rpx; + color: #e8e8e8; + line-height: 1.5; + margin-bottom: 8rpx; + font-family: 'Courier New', monospace; + word-break: break-all; +} + +.empty-logs { + color: #666; + text-align: center; + padding: 60rpx 0; + font-size: 28rpx; +} + +/* 统计区域 */ +.stats-section { + padding: 0 30rpx; +} + +.stats-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20rpx; +} + +.stat-item { + background-color: white; + border-radius: 16rpx; + padding: 30rpx; + text-align: center; + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1); +} + +.stat-value { + font-size: 36rpx; + font-weight: 600; + color: #667eea; + margin-bottom: 8rpx; +} + +.stat-label { + font-size: 24rpx; + color: #666; +} diff --git a/project.config.json b/project.config.json new file mode 100644 index 0000000..6bd5508 --- /dev/null +++ b/project.config.json @@ -0,0 +1,42 @@ +{ + "appid": "wxc4b163d7a28b5041", + "compileType": "miniprogram", + "libVersion": "3.8.10", + "packOptions": { + "ignore": [], + "include": [] + }, + "setting": { + "coverView": true, + "es6": true, + "postcss": true, + "minified": true, + "enhance": true, + "showShadowRootInWxmlPanel": true, + "packNpmRelationList": [], + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + }, + "compileWorklet": false, + "uglifyFileName": false, + "uploadWithSourceMap": true, + "packNpmManually": false, + "minifyWXSS": true, + "minifyWXML": true, + "localPlugins": false, + "disableUseStrict": false, + "useCompilerPlugins": false, + "condition": false, + "swc": false, + "disableSWC": true + }, + "condition": {}, + "editorSetting": { + "tabIndent": "insertSpaces", + "tabSize": 4 + }, + "projectArchitecture": "miniProgram", + "simulatorPluginLibVersion": {} +} \ No newline at end of file diff --git a/project.private.config.json b/project.private.config.json new file mode 100644 index 0000000..c82263a --- /dev/null +++ b/project.private.config.json @@ -0,0 +1,24 @@ +{ + "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", + "projectname": "miniprogram", + "setting": { + "compileHotReLoad": true, + "skylineRenderEnable": true, + "urlCheck": false, + "coverView": true, + "lazyloadPlaceholderEnable": false, + "preloadBackgroundData": false, + "autoAudits": false, + "useApiHook": true, + "useApiHostProcess": true, + "showShadowRootInWxmlPanel": true, + "useStaticServer": false, + "useLanDebug": false, + "showES6CompileOption": false, + "checkInvalidKey": true, + "ignoreDevUnusedFiles": true, + "bigPackageSizeSupport": false + }, + "libVersion": "3.8.10", + "condition": {} +} \ No newline at end of file diff --git a/sitemap.json b/sitemap.json new file mode 100644 index 0000000..cd24f35 --- /dev/null +++ b/sitemap.json @@ -0,0 +1,7 @@ +{ + "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", + "rules": [{ + "action": "allow", + "page": "*" + }] +} \ No newline at end of file diff --git a/styles/components.wxss b/styles/components.wxss new file mode 100644 index 0000000..bcdaafc --- /dev/null +++ b/styles/components.wxss @@ -0,0 +1,503 @@ +/* 通用组件样式库 - 现代化UI组件 */ + +/* ===== 按钮组件 ===== */ +.btn { + display: flex; + align-items: center; + justify-content: center; + min-height: 88rpx; + padding: 0 32rpx; + border-radius: 12rpx; + font-size: 28rpx; + font-weight: 500; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border: none; + cursor: pointer; + position: relative; + overflow: hidden; +} + +.btn::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.1); + opacity: 0; + transition: opacity 0.3s ease; +} + +.btn:active::before { + opacity: 1; +} + +.btn-primary { + background: linear-gradient(135deg, var(--primary-500), var(--primary-600)); + color: white; + box-shadow: 0 4rpx 16rpx rgba(76, 175, 80, 0.3); +} + +.btn-primary:active { + transform: translateY(2rpx); + box-shadow: 0 2rpx 8rpx rgba(76, 175, 80, 0.3); +} + +.btn-secondary { + background: var(--bg-primary); + color: var(--text-primary); + border: 2rpx solid var(--border-medium); +} + +.btn-secondary:active { + background: var(--bg-secondary); + border-color: var(--border-dark); +} + +.btn-ghost { + background: transparent; + color: var(--primary-500); + border: 2rpx solid var(--primary-500); +} + +.btn-ghost:active { + background: rgba(76, 175, 80, 0.1); +} + +.btn-text { + background: transparent; + color: var(--primary-500); + min-height: 64rpx; + padding: 0 16rpx; +} + +.btn-text:active { + background: rgba(76, 175, 80, 0.1); +} + +.btn-sm { + min-height: 64rpx; + padding: 0 24rpx; + font-size: 24rpx; + border-radius: 8rpx; +} + +.btn-lg { + min-height: 96rpx; + padding: 0 48rpx; + font-size: 32rpx; + border-radius: 16rpx; +} + +.btn-disabled { + opacity: 0.5; + pointer-events: none; +} + +/* ===== 卡片组件 ===== */ +.card { + background: var(--bg-primary); + border-radius: 16rpx; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08); + overflow: hidden; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.card:active { + transform: scale(0.98); + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.12); +} + +.card-header { + padding: 24rpx; + border-bottom: 1rpx solid var(--border-light); +} + +.card-body { + padding: 24rpx; +} + +.card-footer { + padding: 24rpx; + border-top: 1rpx solid var(--border-light); + background: var(--bg-secondary); +} + +.card-elevated { + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.12); +} + +.card-flat { + box-shadow: none; + border: 1rpx solid var(--border-light); +} + +/* ===== 输入框组件 ===== */ +.input-group { + margin-bottom: 24rpx; +} + +.input-label { + display: block; + font-size: 24rpx; + font-weight: 500; + color: var(--text-secondary); + margin-bottom: 8rpx; +} + +.input { + width: 100%; + height: 88rpx; + padding: 0 24rpx; + background: var(--bg-primary); + border: 2rpx solid var(--border-medium); + border-radius: 12rpx; + font-size: 28rpx; + color: var(--text-primary); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-sizing: border-box; +} + +.input:focus { + border-color: var(--primary-500); + box-shadow: 0 0 0 6rpx rgba(76, 175, 80, 0.1); +} + +.input::placeholder { + color: var(--text-tertiary); +} + +.input-error { + border-color: var(--error); +} + +.input-error:focus { + border-color: var(--error); + box-shadow: 0 0 0 6rpx rgba(244, 67, 54, 0.1); +} + +.input-success { + border-color: var(--success); +} + +.input-textarea { + height: 160rpx; + padding: 24rpx; + resize: none; +} + +/* ===== 头像组件 ===== */ +.avatar { + display: flex; + align-items: center; + justify-content: center; + background: var(--primary-100); + color: var(--primary-600); + font-weight: 600; + overflow: hidden; + position: relative; +} + +.avatar-xs { + width: 48rpx; + height: 48rpx; + border-radius: 12rpx; + font-size: 20rpx; +} + +.avatar-sm { + width: 64rpx; + height: 64rpx; + border-radius: 16rpx; + font-size: 24rpx; +} + +.avatar-md { + width: 80rpx; + height: 80rpx; + border-radius: 20rpx; + font-size: 28rpx; +} + +.avatar-lg { + width: 120rpx; + height: 120rpx; + border-radius: 30rpx; + font-size: 36rpx; +} + +.avatar-xl { + width: 160rpx; + height: 160rpx; + border-radius: 40rpx; + font-size: 48rpx; +} + +.avatar-round { + border-radius: 50%; +} + +.avatar-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.avatar-online::after { + content: ''; + position: absolute; + bottom: 0; + right: 0; + width: 24rpx; + height: 24rpx; + background: var(--success); + border: 4rpx solid var(--bg-primary); + border-radius: 50%; +} + +/* ===== 徽章组件 ===== */ +.badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 32rpx; + height: 32rpx; + padding: 0 8rpx; + background: var(--error); + color: white; + font-size: 20rpx; + font-weight: 600; + border-radius: 16rpx; + line-height: 1; +} + +.badge-sm { + min-width: 24rpx; + height: 24rpx; + padding: 0 6rpx; + font-size: 18rpx; + border-radius: 12rpx; +} + +.badge-lg { + min-width: 40rpx; + height: 40rpx; + padding: 0 12rpx; + font-size: 22rpx; + border-radius: 20rpx; +} + +.badge-dot { + width: 16rpx; + height: 16rpx; + min-width: 16rpx; + padding: 0; + border-radius: 50%; +} + +.badge-primary { + background: var(--primary-500); +} + +.badge-success { + background: var(--success); +} + +.badge-warning { + background: var(--warning); +} + +.badge-info { + background: var(--info); +} + +/* ===== 分割线组件 ===== */ +.divider { + height: 1rpx; + background: var(--border-light); + margin: 24rpx 0; +} + +.divider-thick { + height: 16rpx; + background: var(--bg-secondary); + margin: 0; +} + +.divider-text { + display: flex; + align-items: center; + margin: 24rpx 0; + color: var(--text-tertiary); + font-size: 24rpx; +} + +.divider-text::before, +.divider-text::after { + content: ''; + flex: 1; + height: 1rpx; + background: var(--border-light); +} + +.divider-text::before { + margin-right: 16rpx; +} + +.divider-text::after { + margin-left: 16rpx; +} + +/* ===== 加载组件 ===== */ +.loading { + display: flex; + align-items: center; + justify-content: center; + padding: 48rpx; +} + +.loading-spinner { + width: 48rpx; + height: 48rpx; + border: 4rpx solid var(--border-light); + border-top: 4rpx solid var(--primary-500); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-text { + margin-left: 16rpx; + color: var(--text-secondary); + font-size: 24rpx; +} + +/* ===== 空状态组件 ===== */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 96rpx 48rpx; + text-align: center; +} + +.empty-icon { + width: 120rpx; + height: 120rpx; + margin-bottom: 24rpx; + opacity: 0.5; +} + +.empty-title { + font-size: 32rpx; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 8rpx; +} + +.empty-description { + font-size: 24rpx; + color: var(--text-secondary); + line-height: 1.5; + margin-bottom: 32rpx; +} + +/* ===== 列表项组件 ===== */ +.list-item { + display: flex; + align-items: center; + padding: 24rpx; + background: var(--bg-primary); + transition: background-color 0.3s ease; + min-height: 88rpx; + box-sizing: border-box; +} + +.list-item:active { + background: var(--bg-secondary); +} + +.list-item-avatar { + margin-right: 24rpx; +} + +.list-item-content { + flex: 1; + min-width: 0; +} + +.list-item-title { + font-size: 28rpx; + font-weight: 500; + color: var(--text-primary); + margin-bottom: 4rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.list-item-subtitle { + font-size: 24rpx; + color: var(--text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.list-item-action { + margin-left: 16rpx; + flex-shrink: 0; +} + +.list-item-border { + border-bottom: 1rpx solid var(--border-light); +} + +/* ===== 标签组件 ===== */ +.tag { + display: inline-flex; + align-items: center; + padding: 8rpx 16rpx; + background: var(--bg-secondary); + color: var(--text-secondary); + font-size: 22rpx; + font-weight: 500; + border-radius: 8rpx; + border: 1rpx solid var(--border-light); +} + +.tag-primary { + background: rgba(76, 175, 80, 0.1); + color: var(--primary-600); + border-color: rgba(76, 175, 80, 0.2); +} + +.tag-success { + background: rgba(76, 175, 80, 0.1); + color: var(--success); + border-color: rgba(76, 175, 80, 0.2); +} + +.tag-warning { + background: rgba(255, 152, 0, 0.1); + color: var(--warning); + border-color: rgba(255, 152, 0, 0.2); +} + +.tag-error { + background: rgba(244, 67, 54, 0.1); + color: var(--error); + border-color: rgba(244, 67, 54, 0.2); +} + +.tag-sm { + padding: 4rpx 12rpx; + font-size: 20rpx; + border-radius: 6rpx; +} diff --git a/styles/design-system.wxss b/styles/design-system.wxss new file mode 100644 index 0000000..157e56e --- /dev/null +++ b/styles/design-system.wxss @@ -0,0 +1,251 @@ +/* 全局设计系统 - 基于Material Design 3和现代移动端最佳实践 */ + +/* ===== 颜色系统 ===== */ +:root { + /* 主色调 - 基于微信绿色调优化 */ + --primary-50: #e8f5e8; + --primary-100: #c8e6c9; + --primary-200: #a5d6a7; + --primary-300: #81c784; + --primary-400: #66bb6a; + --primary-500: #4caf50; + --primary-600: #43a047; + --primary-700: #388e3c; + --primary-800: #2e7d32; + --primary-900: #1b5e20; + + /* 中性色 - 现代化灰色系统 */ + --neutral-0: #ffffff; + --neutral-50: #fafafa; + --neutral-100: #f5f5f5; + --neutral-200: #eeeeee; + --neutral-300: #e0e0e0; + --neutral-400: #bdbdbd; + --neutral-500: #9e9e9e; + --neutral-600: #757575; + --neutral-700: #616161; + --neutral-800: #424242; + --neutral-900: #212121; + + /* 语义化颜色 */ + --success: #4caf50; + --warning: #ff9800; + --error: #f44336; + --info: #2196f3; + + /* 背景色 */ + --bg-primary: #ffffff; + --bg-secondary: #fafafa; + --bg-tertiary: #f5f5f5; + --bg-overlay: rgba(0, 0, 0, 0.5); + + /* 文字颜色 */ + --text-primary: #212121; + --text-secondary: #757575; + --text-tertiary: #bdbdbd; + --text-inverse: #ffffff; + + /* 边框颜色 */ + --border-light: #f0f0f0; + --border-medium: #e0e0e0; + --border-dark: #d0d0d0; +} + +/* ===== 字体系统 ===== */ +.text-display { + font-size: 48rpx; + font-weight: 700; + line-height: 1.2; +} + +.text-headline { + font-size: 40rpx; + font-weight: 600; + line-height: 1.3; +} + +.text-title { + font-size: 36rpx; + font-weight: 600; + line-height: 1.4; +} + +.text-subtitle { + font-size: 32rpx; + font-weight: 500; + line-height: 1.4; +} + +.text-body { + font-size: 28rpx; + font-weight: 400; + line-height: 1.5; +} + +.text-caption { + font-size: 24rpx; + font-weight: 400; + line-height: 1.4; +} + +.text-label { + font-size: 22rpx; + font-weight: 500; + line-height: 1.3; + letter-spacing: 0.5rpx; +} + +/* ===== 间距系统 ===== */ +.p-xs { padding: 8rpx; } +.p-sm { padding: 16rpx; } +.p-md { padding: 24rpx; } +.p-lg { padding: 32rpx; } +.p-xl { padding: 48rpx; } + +.px-xs { padding-left: 8rpx; padding-right: 8rpx; } +.px-sm { padding-left: 16rpx; padding-right: 16rpx; } +.px-md { padding-left: 24rpx; padding-right: 24rpx; } +.px-lg { padding-left: 32rpx; padding-right: 32rpx; } +.px-xl { padding-left: 48rpx; padding-right: 48rpx; } + +.py-xs { padding-top: 8rpx; padding-bottom: 8rpx; } +.py-sm { padding-top: 16rpx; padding-bottom: 16rpx; } +.py-md { padding-top: 24rpx; padding-bottom: 24rpx; } +.py-lg { padding-top: 32rpx; padding-bottom: 32rpx; } +.py-xl { padding-top: 48rpx; padding-bottom: 48rpx; } + +.m-xs { margin: 8rpx; } +.m-sm { margin: 16rpx; } +.m-md { margin: 24rpx; } +.m-lg { margin: 32rpx; } +.m-xl { margin: 48rpx; } + +.mx-xs { margin-left: 8rpx; margin-right: 8rpx; } +.mx-sm { margin-left: 16rpx; margin-right: 16rpx; } +.mx-md { margin-left: 24rpx; margin-right: 24rpx; } +.mx-lg { margin-left: 32rpx; margin-right: 32rpx; } +.mx-xl { margin-left: 48rpx; margin-right: 48rpx; } + +.my-xs { margin-top: 8rpx; margin-bottom: 8rpx; } +.my-sm { margin-top: 16rpx; margin-bottom: 16rpx; } +.my-md { margin-top: 24rpx; margin-bottom: 24rpx; } +.my-lg { margin-top: 32rpx; margin-bottom: 32rpx; } +.my-xl { margin-top: 48rpx; margin-bottom: 48rpx; } + +/* ===== 圆角系统 ===== */ +.rounded-none { border-radius: 0; } +.rounded-xs { border-radius: 4rpx; } +.rounded-sm { border-radius: 8rpx; } +.rounded-md { border-radius: 12rpx; } +.rounded-lg { border-radius: 16rpx; } +.rounded-xl { border-radius: 24rpx; } +.rounded-full { border-radius: 50%; } + +/* ===== 阴影系统 ===== */ +.shadow-none { box-shadow: none; } +.shadow-sm { box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06); } +.shadow-md { box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08); } +.shadow-lg { box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.12); } +.shadow-xl { box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.16); } + +/* ===== 布局系统 ===== */ +.flex { display: flex; } +.flex-col { flex-direction: column; } +.flex-row { flex-direction: row; } +.items-center { align-items: center; } +.items-start { align-items: flex-start; } +.items-end { align-items: flex-end; } +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } +.justify-around { justify-content: space-around; } +.justify-start { justify-content: flex-start; } +.justify-end { justify-content: flex-end; } + +.flex-1 { flex: 1; } +.flex-none { flex: none; } + +/* ===== 定位系统 ===== */ +.relative { position: relative; } +.absolute { position: absolute; } +.fixed { position: fixed; } + +/* ===== 宽高系统 ===== */ +.w-full { width: 100%; } +.h-full { height: 100%; } +.w-screen { width: 100vw; } +.h-screen { height: 100vh; } + +/* ===== 透明度系统 ===== */ +.opacity-0 { opacity: 0; } +.opacity-25 { opacity: 0.25; } +.opacity-50 { opacity: 0.5; } +.opacity-75 { opacity: 0.75; } +.opacity-100 { opacity: 1; } + +/* ===== 过渡动画系统 ===== */ +.transition-all { transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } +.transition-opacity { transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); } +.transition-transform { transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); } + +/* ===== 变换系统 ===== */ +.scale-95 { transform: scale(0.95); } +.scale-100 { transform: scale(1); } +.scale-105 { transform: scale(1.05); } + +.translate-y-full { transform: translateY(100%); } +.translate-y-0 { transform: translateY(0); } + +/* ===== 安全区域适配 ===== */ +.safe-area-top { + padding-top: constant(safe-area-inset-top); + padding-top: env(safe-area-inset-top); +} + +.safe-area-bottom { + padding-bottom: constant(safe-area-inset-bottom); + padding-bottom: env(safe-area-inset-bottom); +} + +.safe-area-left { + padding-left: constant(safe-area-inset-left); + padding-left: env(safe-area-inset-left); +} + +.safe-area-right { + padding-right: constant(safe-area-inset-right); + padding-right: env(safe-area-inset-right); +} + +/* ===== 触摸友好 ===== */ +.touch-target { + min-height: 88rpx; /* 44px */ + min-width: 88rpx; +} + +/* ===== 文字颜色工具类 ===== */ +.text-primary { color: var(--text-primary); } +.text-secondary { color: var(--text-secondary); } +.text-tertiary { color: var(--text-tertiary); } +.text-inverse { color: var(--text-inverse); } +.text-success { color: var(--success); } +.text-warning { color: var(--warning); } +.text-error { color: var(--error); } +.text-info { color: var(--info); } + +/* ===== 背景颜色工具类 ===== */ +.bg-primary { background-color: var(--bg-primary); } +.bg-secondary { background-color: var(--bg-secondary); } +.bg-tertiary { background-color: var(--bg-tertiary); } +.bg-success { background-color: var(--success); } +.bg-warning { background-color: var(--warning); } +.bg-error { background-color: var(--error); } +.bg-info { background-color: var(--info); } + +/* ===== 边框工具类 ===== */ +.border { border: 1rpx solid var(--border-medium); } +.border-light { border: 1rpx solid var(--border-light); } +.border-dark { border: 1rpx solid var(--border-dark); } +.border-t { border-top: 1rpx solid var(--border-medium); } +.border-b { border-bottom: 1rpx solid var(--border-medium); } +.border-l { border-left: 1rpx solid var(--border-medium); } +.border-r { border-right: 1rpx solid var(--border-medium); } diff --git a/styles/responsive.wxss b/styles/responsive.wxss new file mode 100644 index 0000000..bd5ab8b --- /dev/null +++ b/styles/responsive.wxss @@ -0,0 +1,403 @@ +/* 响应式布局系统 - 基于全球最佳实践 */ + +/* ===== 断点系统 ===== */ +/* 基于主流设备尺寸定义断点 */ + +/* 小屏手机: 320-480px (iPhone SE, 小屏Android) */ +@media (max-width: 480px) { + .container { + padding: 16rpx; + } + + .text-responsive { + font-size: 26rpx; + } + + .btn-responsive { + min-height: 80rpx; + font-size: 26rpx; + } + + .card-responsive { + margin: 8rpx; + padding: 16rpx; + } + + .avatar-responsive { + width: 64rpx; + height: 64rpx; + } +} + +/* 中屏手机: 481-600px (iPhone 6/7/8, 中等Android) */ +@media (min-width: 481px) and (max-width: 600px) { + .container { + padding: 24rpx; + } + + .text-responsive { + font-size: 28rpx; + } + + .btn-responsive { + min-height: 88rpx; + font-size: 28rpx; + } + + .card-responsive { + margin: 12rpx; + padding: 20rpx; + } + + .avatar-responsive { + width: 72rpx; + height: 72rpx; + } +} + +/* 大屏手机: 601-768px (iPhone Plus/Pro, 大屏Android) */ +@media (min-width: 601px) and (max-width: 768px) { + .container { + padding: 32rpx; + } + + .text-responsive { + font-size: 30rpx; + } + + .btn-responsive { + min-height: 96rpx; + font-size: 30rpx; + } + + .card-responsive { + margin: 16rpx; + padding: 24rpx; + } + + .avatar-responsive { + width: 80rpx; + height: 80rpx; + } +} + +/* 平板设备: 769px+ (iPad, 大屏平板) */ +@media (min-width: 769px) { + .container { + padding: 48rpx; + max-width: 1200rpx; + margin: 0 auto; + } + + .text-responsive { + font-size: 32rpx; + } + + .btn-responsive { + min-height: 104rpx; + font-size: 32rpx; + } + + .card-responsive { + margin: 20rpx; + padding: 32rpx; + } + + .avatar-responsive { + width: 96rpx; + height: 96rpx; + } + + /* 平板专用布局 */ + .tablet-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300rpx, 1fr)); + gap: 24rpx; + } + + .tablet-sidebar { + width: 300rpx; + position: fixed; + left: 0; + top: 0; + bottom: 0; + background: var(--bg-primary); + border-right: 1rpx solid var(--border-light); + } + + .tablet-main { + margin-left: 300rpx; + padding: 24rpx; + } +} + +/* ===== 屏幕密度适配 ===== */ +/* 基于设备像素比优化显示效果 */ + +/* 低密度屏幕 (dpr <= 1.5) */ +@media (-webkit-max-device-pixel-ratio: 1.5) { + .border-thin { + border-width: 1rpx; + } + + .shadow-responsive { + box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1); + } +} + +/* 中密度屏幕 (1.5 < dpr <= 2.5) */ +@media (-webkit-min-device-pixel-ratio: 1.5) and (-webkit-max-device-pixel-ratio: 2.5) { + .border-thin { + border-width: 1rpx; + } + + .shadow-responsive { + box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.12); + } +} + +/* 高密度屏幕 (dpr > 2.5) */ +@media (-webkit-min-device-pixel-ratio: 2.5) { + .border-thin { + border-width: 2rpx; + } + + .shadow-responsive { + box-shadow: 0 6rpx 12rpx rgba(0, 0, 0, 0.15); + } +} + +/* ===== 方向适配 ===== */ +/* 横屏和竖屏的不同布局 */ + +/* 竖屏模式 */ +@media (orientation: portrait) { + .orientation-adaptive { + flex-direction: column; + } + + .portrait-only { + display: block; + } + + .landscape-only { + display: none; + } + + .portrait-stack { + flex-direction: column; + } + + .portrait-full-width { + width: 100%; + } +} + +/* 横屏模式 */ +@media (orientation: landscape) { + .orientation-adaptive { + flex-direction: row; + } + + .portrait-only { + display: none; + } + + .landscape-only { + display: block; + } + + .landscape-split { + flex-direction: row; + } + + .landscape-half-width { + width: 50%; + } + + /* 横屏时减少垂直间距 */ + .landscape-compact { + padding-top: 16rpx; + padding-bottom: 16rpx; + } +} + +/* ===== 触摸友好设计 ===== */ +/* 基于人体工程学的触摸区域设计 */ + +/* 最小触摸目标 (44px = 88rpx) */ +.touch-target-min { + min-width: 88rpx; + min-height: 88rpx; +} + +/* 推荐触摸目标 (48px = 96rpx) */ +.touch-target-recommended { + min-width: 96rpx; + min-height: 96rpx; +} + +/* 大触摸目标 (56px = 112rpx) */ +.touch-target-large { + min-width: 112rpx; + min-height: 112rpx; +} + +/* 触摸间距 (8px = 16rpx) */ +.touch-spacing { + margin: 16rpx; +} + +/* ===== 可访问性增强 ===== */ +/* 提升用户体验的可访问性设计 */ + +/* 高对比度模式 */ +@media (prefers-contrast: high) { + .accessible-text { + color: #000000; + background: #ffffff; + } + + .accessible-border { + border: 2rpx solid #000000; + } +} + +/* 减少动画模式 */ +@media (prefers-reduced-motion: reduce) { + .motion-safe { + animation: none; + transition: none; + } +} + +/* 深色模式适配 */ +@media (prefers-color-scheme: dark) { + .dark-adaptive { + background: #1a1a1a; + color: #ffffff; + } + + .dark-card { + background: #2a2a2a; + border-color: #404040; + } + + .dark-border { + border-color: #404040; + } +} + +/* ===== 网格系统 ===== */ +/* 灵活的响应式网格布局 */ + +.grid { + display: grid; + gap: 24rpx; +} + +.grid-1 { grid-template-columns: 1fr; } +.grid-2 { grid-template-columns: repeat(2, 1fr); } +.grid-3 { grid-template-columns: repeat(3, 1fr); } +.grid-4 { grid-template-columns: repeat(4, 1fr); } + +/* 响应式网格 */ +.grid-responsive { + display: grid; + gap: 24rpx; + grid-template-columns: repeat(auto-fit, minmax(280rpx, 1fr)); +} + +/* 网格项目 */ +.grid-item { + min-width: 0; /* 防止内容溢出 */ +} + +.grid-span-2 { grid-column: span 2; } +.grid-span-3 { grid-column: span 3; } +.grid-span-4 { grid-column: span 4; } + +/* ===== 弹性布局增强 ===== */ +/* 现代化的Flexbox布局工具 */ + +.flex-responsive { + display: flex; + flex-wrap: wrap; + gap: 16rpx; +} + +.flex-item { + flex: 1 1 auto; + min-width: 0; +} + +.flex-item-grow { + flex-grow: 1; +} + +.flex-item-shrink { + flex-shrink: 1; +} + +.flex-item-no-shrink { + flex-shrink: 0; +} + +/* ===== 容器查询模拟 ===== */ +/* 基于容器大小的样式调整 */ + +.container-sm { + max-width: 640rpx; +} + +.container-md { + max-width: 768rpx; +} + +.container-lg { + max-width: 1024rpx; +} + +.container-xl { + max-width: 1280rpx; +} + +.container-responsive { + width: 100%; + margin: 0 auto; + padding: 0 24rpx; +} + +/* ===== 文字响应式 ===== */ +/* 基于屏幕大小的文字缩放 */ + +.text-scale-sm { + font-size: clamp(20rpx, 2.5vw, 24rpx); +} + +.text-scale-md { + font-size: clamp(24rpx, 3vw, 28rpx); +} + +.text-scale-lg { + font-size: clamp(28rpx, 3.5vw, 32rpx); +} + +.text-scale-xl { + font-size: clamp(32rpx, 4vw, 40rpx); +} + +/* ===== 间距响应式 ===== */ +/* 基于屏幕大小的间距调整 */ + +.spacing-responsive { + padding: clamp(16rpx, 3vw, 32rpx); +} + +.margin-responsive { + margin: clamp(8rpx, 2vw, 24rpx); +} + +.gap-responsive { + gap: clamp(12rpx, 2.5vw, 24rpx); +} diff --git a/styles/screen-adaption.wxss b/styles/screen-adaption.wxss new file mode 100644 index 0000000..244eff5 --- /dev/null +++ b/styles/screen-adaption.wxss @@ -0,0 +1,222 @@ +/* 小程序屏幕适配通用样式 - 解决滚动条问题 */ +/* 基于最新的微信小程序最佳实践 */ + +/* 全局页面样式 - 禁用滚动 */ +page { + height: 100%; + overflow: hidden; + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* 通用页面容器 - 全屏高度适配 */ +.page-container { + height: 100vh; + min-height: 100vh; + max-height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; + box-sizing: border-box; + position: relative; +} + +/* 适配安全区域 */ +.safe-area-container { + height: 100vh; + 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); + box-sizing: border-box; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* 内容区域 - 可滚动 */ +.scrollable-content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + box-sizing: border-box; +} + +/* 固定头部 */ +.fixed-header { + position: sticky; + top: 0; + z-index: 1000; + flex-shrink: 0; +} + +/* 固定底部 */ +.fixed-footer { + position: sticky; + bottom: 0; + z-index: 1000; + flex-shrink: 0; +} + +/* 适配不同屏幕尺寸的媒体查询 */ +/* iPhone SE (375x667) */ +@media screen and (max-width: 375px) { + .page-container { + font-size: 28rpx; + } +} + +/* iPhone 12/13/14 (390x844) */ +@media screen and (max-width: 390px) and (min-height: 844px) { + .page-container { + height: 100vh; + } +} + +/* iPhone 12/13/14 Pro Max (428x926) */ +@media screen and (max-width: 428px) and (min-height: 926px) { + .page-container { + height: 100vh; + } +} + +/* iPad适配 */ +@media screen and (min-width: 768px) { + .page-container { + max-width: 750rpx; + margin: 0 auto; + } +} + +/* 禁用选择和长按 */ +.no-select { + -webkit-user-select: none; + user-select: none; + -webkit-touch-callout: none; +} + +/* 防止点击穿透 */ +.prevent-touch { + pointer-events: none; +} + +.allow-touch { + pointer-events: auto; +} + +/* 通用动画 */ +.fade-in { + animation: fadeIn 0.3s ease-in-out; +} + +.slide-up { + animation: slideUp 0.3s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + transform: translateY(20rpx); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* 响应式字体大小 */ +.responsive-text { + font-size: clamp(24rpx, 2.5vw, 32rpx); +} + +.responsive-title { + font-size: clamp(32rpx, 4vw, 48rpx); +} + +.responsive-subtitle { + font-size: clamp(26rpx, 3vw, 36rpx); +} + +/* 通用间距 */ +.spacing-xs { margin: 8rpx; } +.spacing-sm { margin: 16rpx; } +.spacing-md { margin: 24rpx; } +.spacing-lg { margin: 32rpx; } +.spacing-xl { margin: 48rpx; } + +.padding-xs { padding: 8rpx; } +.padding-sm { padding: 16rpx; } +.padding-md { padding: 24rpx; } +.padding-lg { padding: 32rpx; } +.padding-xl { padding: 48rpx; } + +/* Flexbox布局助手 */ +.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; +} + +.flex-row { + display: flex; + flex-direction: row; +} + +.flex-1 { + flex: 1; +} + +.flex-shrink-0 { + flex-shrink: 0; +} + +/* 通用圆角 */ +.rounded-sm { border-radius: 8rpx; } +.rounded-md { border-radius: 16rpx; } +.rounded-lg { border-radius: 24rpx; } +.rounded-xl { border-radius: 32rpx; } +.rounded-full { border-radius: 50%; } + +/* 通用阴影 */ +.shadow-sm { + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); +} + +.shadow-md { + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15); +} + +.shadow-lg { + box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.2); +} + +/* 调试模式 - 开发时使用 */ +.debug-border { + border: 2rpx solid red !important; +} + +.debug-bg { + background: rgba(255, 0, 0, 0.1) !important; +} \ No newline at end of file diff --git a/test-feedback.js b/test-feedback.js new file mode 100644 index 0000000..80b6548 --- /dev/null +++ b/test-feedback.js @@ -0,0 +1,35 @@ +// 测试反馈提交功能 +const ApiClient = require('./utils/api-client.js'); + +// 创建测试函数 +async function testFeedbackSubmission() { + console.log('开始测试反馈提交功能...'); + + // 初始化API客户端 + const apiClient = new ApiClient(); + + try { + // 模拟提交反馈 + const testFeedback = '这是一个测试反馈内容,用于验证提交功能。'; + console.log(`提交反馈: ${testFeedback}`); + + const response = await apiClient.post('/api/v1/user/feedback', { + content: testFeedback, + type: 'suggestion', + source: 'miniprogram' + }); + + console.log('反馈提交成功!', response); + return { success: true, response }; + } catch (error) { + console.error('反馈提交失败!', error); + return { success: false, error: error.message }; + } +} + +// 执行测试 +if (require.main === module) { + testFeedbackSubmission(); +} + +module.exports = { testFeedbackSubmission }; \ No newline at end of file diff --git a/test-friends-layout.html b/test-friends-layout.html new file mode 100644 index 0000000..60644ce --- /dev/null +++ b/test-friends-layout.html @@ -0,0 +1,323 @@ + + + + + + 好友页面布局预览 + + + +
+ + + + +
+ +
+
+
+ 👥 +
3
+
+
+
新的朋友
+
3 个新请求
+
+
+
+ +
+
👨‍👩‍👧‍👦
+
+
群聊
+
查看群聊列表
+
+
+
+
+ + +
+
+
联系人
+
+ +
+
+
+ 张 +
+
+
+
张三
+
在线
+
+
+
💬
+
📹
+
+
+ +
+
+
+
李四
+
今天很开心
+
+
+
💬
+
📹
+
+
+ +
+
+
+
王五
+
离线
+
+
+
💬
+
📹
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/test.js b/test.js new file mode 100644 index 0000000..34a1769 --- /dev/null +++ b/test.js @@ -0,0 +1,393 @@ +import { defineStore } from 'pinia' + +export const useWebSocketStore = defineStore('websocket', () => { + let socketTask = null; + let reconnectTimer = null; + let heartbeatTimer = null; + let reconnectAttempts = 0; + const MAX_RECONNECT_ATTEMPTS = 5; + const HEARTBEAT_INTERVAL = 30000; // 30秒心跳 + + // 连接状态 + const connectionState = ref('disconnected'); // disconnected, connecting, connected, reconnecting + + /** + * 建立WebSocket连接 + * @param {Object} registerInfo - 注册信息 + * @param {string} registerInfo.device_id - 设备ID + * @param {string} registerInfo.token - JWT Token + */ + const buildWebSocket = async (registerInfo) => { + try { + // 确保先关闭已有连接 + if (socketTask) { + socketTask.close({ code: 1000, reason: '重新连接' }); + socketTask = null; + } + + // 清除重连定时器 + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + + connectionState.value = 'connecting'; + + // WebSocket地址 + const wsUrl = `wss://api.faxianwo.me/api/v1/ws?device_id=${registerInfo.device_id}`; + + console.log('开始连接WebSocket:', wsUrl); + + // uniapp WebSocket连接配置 + socketTask = uni.connectSocket({ + url: wsUrl, + header: { + 'Authorization': `Bearer ${registerInfo.token}`, + 'Content-Type': 'application/json' + }, + // 移除protocols参数 + success: () => { + console.log('WebSocket连接请求发送成功'); + }, + fail: (error) => { + console.error('WebSocket连接请求失败:', error); + connectionState.value = 'disconnected'; + handleConnectionError(error); + } + }); + + // 连接成功事件 + socketTask.onOpen((response) => { + console.log('WebSocket连接成功:', response); + connectionState.value = 'connected'; + reconnectAttempts = 0; // 重置重连次数 + + // 发送认证确认消息 + sendAuthConfirmation(); + + // 开始心跳 + startHeartbeat(); + + // 显示连接成功提示 + uni.showToast({ + title: '连接成功', + icon: 'success', + duration: 2000 + }); + }); + + // 接收消息事件 + socketTask.onMessage((message) => { + try { + const data = JSON.parse(message.data); + console.log('收到WebSocket消息:', data); + handleIncomingMessage(data); + } catch (error) { + console.error('解析WebSocket消息失败:', error, message.data); + } + }); + + // 连接错误事件 + socketTask.onError((error) => { + console.error('WebSocket连接错误:', error); + connectionState.value = 'disconnected'; + stopHeartbeat(); + handleConnectionError(error); + }); + + // 连接关闭事件 + socketTask.onClose((closeEvent) => { + console.log('WebSocket连接关闭:', closeEvent); + connectionState.value = 'disconnected'; + stopHeartbeat(); + + // 如果不是主动关闭,尝试重连 + if (closeEvent.code !== 1000 && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + attemptReconnect(registerInfo); + } + }); + + return socketTask; + + } catch (error) { + console.error('建立WebSocket连接异常:', error); + connectionState.value = 'disconnected'; + return null; + } + }; + + /** + * 发送认证确认消息 + */ + const sendAuthConfirmation = () => { + if (!socketTask || connectionState.value !== 'connected') { + console.warn('WebSocket未连接,无法发送认证确认'); + return; + } + + const authMessage = { + type: 'auth_confirm', + id: `auth_${Date.now()}`, + data: { + timestamp: Date.now() + } + }; + + socketTask.send({ + data: JSON.stringify(authMessage), + success: () => console.log('认证确认发送成功'), + fail: (err) => console.error('认证确认发送失败:', err) + }); + }; + + /** + * 开始心跳检测 + */ + const startHeartbeat = () => { + stopHeartbeat(); // 先清除已有的心跳 + + heartbeatTimer = setInterval(() => { + if (socketTask && connectionState.value === 'connected') { + const heartbeatMessage = { + type: 'heartbeat', + id: `hb_${Date.now()}`, + data: { + timestamp: Date.now() + } + }; + + socketTask.send({ + data: JSON.stringify(heartbeatMessage), + success: () => console.log('心跳发送成功'), + fail: (err) => { + console.error('心跳发送失败:', err); + // 心跳失败可能表示连接有问题 + connectionState.value = 'disconnected'; + } + }); + } + }, HEARTBEAT_INTERVAL); + }; + + /** + * 停止心跳检测 + */ + const stopHeartbeat = () => { + if (heartbeatTimer) { + clearInterval(heartbeatTimer); + heartbeatTimer = null; + } + }; + + /** + * 处理接收到的消息 + */ + const handleIncomingMessage = (message) => { + switch (message.type) { + case 'new_message': + // 处理新消息 + console.log('收到新消息:', message.data); + break; + + case 'message_read': + // 处理已读回执 + console.log('消息已读:', message.data); + break; + + case 'message_recalled': + // 处理消息撤回 + console.log('消息撤回:', message.data); + break; + + case 'heartbeat_response': + // 心跳响应 + console.log('收到心跳响应'); + break; + + default: + console.log('收到其他类型消息:', message); + } + }; + + /** + * 发送消息的通用方法 + */ + const sendMessage = (messageType, messageData) => { + if (!socketTask || connectionState.value !== 'connected') { + console.warn('WebSocket未连接,无法发送消息'); + return false; + } + + const message = { + type: messageType, + id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + data: messageData + }; + + socketTask.send({ + data: JSON.stringify(message), + success: () => console.log(` ${messageType} 发送成功`), + fail: (err) => console.error(` ${messageType} 发送失败:`, err) + }); + + return true; + }; + + /** + * 发送聊天消息 + */ + const sendChatMessage = (receiverId, content, msgType = 0, chatType = 0) => { + return sendMessage('send_message', { + receiverId: String(receiverId), // 确保是字符串格式 + chatType, + msgType, + content, + timestamp: Date.now() + }); + }; + + /** + * 处理连接错误 + */ + const handleConnectionError = (error) => { + let errorMessage = '连接错误'; + + // 完善的错误码处理 + switch (error.errCode) { + case 1000: + errorMessage = '正常关闭'; + break; + case 1001: + errorMessage = '连接断开'; + break; + case 1002: + errorMessage = '协议错误'; + break; + case 1003: + errorMessage = '数据格式错误'; + break; + case 1006: + errorMessage = '异常断开'; + break; + case 1011: + errorMessage = '服务器错误'; + break; + case 1012: + errorMessage = '服务重启'; + break; + default: + console.log(`未知错误代码: ${error.errCode}`); + errorMessage = `连接错误 (${error.errCode})`; + } + + console.error(` ${errorMessage}:`, error); + + // 只有在非正常关闭时才显示错误提示 + if (error.errCode !== 1000) { + uni.showToast({ + title: errorMessage, + icon: 'none', + duration: 3000 + }); + } + }; + + /** + * 重连机制 + */ + const attemptReconnect = (registerInfo) => { + if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { + console.error('已达到最大重连次数,停止重连'); + uni.showToast({ + title: '连接失败,请检查网络', + icon: 'none', + duration: 3000 + }); + return; + } + + reconnectAttempts++; + connectionState.value = 'reconnecting'; + + // 指数退避策略 + const delay = Math.min(2000 * Math.pow(1.5, reconnectAttempts), 30000); + + console.log(` 将在 ${delay}ms 后尝试重连 (第${reconnectAttempts}次)`); + + reconnectTimer = setTimeout(async () => { + console.log(`开始重连 (第${reconnectAttempts}次)`); + + try { + const task = await buildWebSocket(registerInfo); + if (task) { + console.log('重连成功'); + } + } catch (error) { + console.error(`重连失败 (第${reconnectAttempts}次):`, error); + // 继续尝试重连 + if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + attemptReconnect(registerInfo); + } + } + }, delay); + }; + + /** + * 关闭WebSocket连接 + */ + const closeWebSocket = () => { + console.log('主动关闭WebSocket连接'); + + // 停止心跳 + stopHeartbeat(); + + // 清除重连定时器 + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + + // 重置重连计数器 + reconnectAttempts = 0; + + // 关闭连接 + if (socketTask) { + socketTask.close({ + code: 1000, // 正常关闭 + reason: '用户主动关闭', + success: () => { + console.log('WebSocket已关闭'); + connectionState.value = 'disconnected'; + }, + fail: (err) => console.error('关闭WebSocket失败:', err) + }); + + socketTask = null; + } + }; + + /** + * 获取连接状态 + */ + const getConnectionState = () => connectionState.value; + + /** + * 检查是否已连接 + */ + const isConnected = () => connectionState.value === 'connected'; + + return { + // 主要方法 + buildWebSocket, + closeWebSocket, + sendMessage, + sendChatMessage, + + // 状态查询 + getConnectionState, + isConnected, + + // 响应式状态(用于UI绑定) + connectionState: readonly(connectionState) + }; +}); \ No newline at end of file diff --git a/utils/account-sync.js b/utils/account-sync.js new file mode 100644 index 0000000..223eafa --- /dev/null +++ b/utils/account-sync.js @@ -0,0 +1,247 @@ +// 账号同步管理器 - 自动检查并处理手机号绑定和账号合并 +const apiClient = require('./api-client.js'); +const authManager = require('./auth.js'); + +class AccountSyncManager { + constructor() { + this.isChecking = false; + this.hasChecked = false; + } + + // 检查用户是否需要绑定手机号 + async checkPhoneBinding() { + try { + // 确保用户已登录 + if (!authManager.isLoggedIn()) { + console.log('用户未登录,跳过手机号绑定检查'); + return false; + } + + const userInfo = authManager.getUserDisplayInfo(); + if (!userInfo) { + console.log('无法获取用户信息,跳过手机号绑定检查'); + return false; + } + + // 检查是否已绑定手机号(注意空字符串的处理) + const hasPhone = !!(userInfo.phone && userInfo.phone.trim() !== ''); + console.log('手机号绑定状态检查:', { + hasPhone, + phone: userInfo.phone || '未绑定', + phoneValue: userInfo.phone + }); + + return !hasPhone; // 返回是否需要绑定 + } catch (error) { + console.error('检查手机号绑定状态失败:', error); + return false; + } + } + + // 自动检查并引导用户绑定手机号 + async autoCheckAndBind() { + if (this.isChecking || this.hasChecked) { + return; + } + + this.isChecking = true; + + try { + console.log('开始自动检查账号同步状态...'); + + const needsBinding = await this.checkPhoneBinding(); + + if (needsBinding) { + console.log('用户需要绑定手机号,显示绑定页面'); + this.showPhoneBindingModal(); + } else { + console.log('用户已绑定手机号,无需处理'); + } + + this.hasChecked = true; + } catch (error) { + console.error('自动检查账号同步失败:', error); + } finally { + this.isChecking = false; + } + } + + // 显示手机号绑定弹窗 + showPhoneBindingModal() { + console.log('🔥🔥🔥 showPhoneBindingModal 方法被调用了!'); + + // 检查是否应该跳过 + if (this.shouldSkipBinding()) { + console.log('用户选择跳过绑定,24小时内不再提示'); + return; + } + + console.log('🔥 准备显示手机号绑定弹窗...'); + wx.showModal({ + title: '完善账号信息', + content: '为了更好的使用体验和账号安全,请绑定您的手机号', + confirmText: '立即绑定', + cancelText: '稍后再说', + success: (res) => { + if (res.confirm) { + // 跳转到手机号绑定页面 + wx.navigateTo({ + url: '/pages/account-sync/phone-binding/phone-binding' + }); + } else { + // 用户选择稍后,记录状态避免频繁弹出 + this.setSkipBindingFlag(); + } + } + }); + } + + // 设置跳过绑定标记(24小时内不再提示) + setSkipBindingFlag() { + const skipUntil = Date.now() + 24 * 60 * 60 * 1000; // 24小时后 + wx.setStorageSync('skipPhoneBinding', skipUntil); + } + + // 检查是否应该跳过绑定提示 + shouldSkipBinding() { + try { + const skipUntil = wx.getStorageSync('skipPhoneBinding'); + if (skipUntil && Date.now() < skipUntil) { + return true; + } + } catch (error) { + console.error('检查跳过绑定标记失败:', error); + } + return false; + } + + // 清除跳过绑定标记 + clearSkipBindingFlag() { + try { + wx.removeStorageSync('skipPhoneBinding'); + } catch (error) { + console.error('清除跳过绑定标记失败:', error); + } + } + + // 绑定手机号 + async bindPhone(phone, verifyCode, autoMerge = false) { + try { + console.log('开始绑定手机号:', { phone, autoMerge }); + + const response = await apiClient.bindPhone(phone, verifyCode, autoMerge); + + if (response && response.code === 0) { // 修正成功码为0 + console.log('手机号绑定成功:', response.data); + + // 清除跳过绑定标记 + this.clearSkipBindingFlag(); + + // 尝试刷新用户信息(失败不影响绑定结果) + try { + await this.refreshUserInfo(); + } catch (refreshError) { + console.warn('刷新用户信息失败,但绑定操作已成功:', refreshError); + // 不抛出错误,因为绑定本身是成功的 + } + + // 根据文档,返回完整的绑定结果 + return { + success: response.data.success || true, + hasMerged: response.data.hasMerged || false, + mergeCount: response.data.mergeCount || 0, + ...response.data + }; + } else { + throw new Error(response?.message || '绑定失败'); + } + } catch (error) { + console.error('绑定手机号失败:', error); + + // 根据文档,处理特定的错误情况 + if (error.message && error.message.includes('已关联其他账号')) { + // 重新抛出冲突错误,让上层处理 + throw new Error('该手机号已关联其他账号,请选择合并或使用其他手机号'); + } + + throw error; + } + } + + // 检测可合并账号 + async detectMerge(customId) { + try { + console.log('检测可合并账号:', customId); + + const response = await apiClient.detectMerge(customId); + + if (response && response.code === 0) { // 修正成功码为0 + console.log('检测可合并账号成功:', response.data); + return response.data; + } else { + throw new Error(response?.message || '检测失败'); + } + } catch (error) { + console.error('检测可合并账号失败:', error); + throw error; + } + } + + // 合并账号 + async mergeAccount(primaryCustomId, secondaryCustomId, mergeReason) { + try { + console.log('开始合并账号:', { primaryCustomId, secondaryCustomId, mergeReason }); + + const response = await apiClient.mergeAccount(primaryCustomId, secondaryCustomId, mergeReason); + + if (response && response.code === 0) { // 修正成功码为0 + console.log('账号合并成功:', response.data); + + // 刷新用户信息 + await this.refreshUserInfo(); + + return response.data; + } else { + throw new Error(response?.message || '合并失败'); + } + } catch (error) { + console.error('合并账号失败:', error); + throw error; + } + } + + // 刷新用户信息 + async refreshUserInfo() { + try { + console.log('刷新用户信息...'); + + const response = await apiClient.getUserInfo(); + if (response && response.code === 0) { + // 更新全局用户信息 + const app = getApp(); + if (app && app.globalData.userInfo) { + app.globalData.userInfo.user = response.data; + + // 同步到本地存储 + wx.setStorageSync('userInfo', app.globalData.userInfo); + + console.log('用户信息刷新成功'); + } + } + } catch (error) { + console.error('刷新用户信息失败:', error); + } + } + + // 重置检查状态(用于测试或强制重新检查) + resetCheckStatus() { + this.hasChecked = false; + this.isChecking = false; + this.clearSkipBindingFlag(); + } +} + +// 创建单例实例 +const accountSyncManager = new AccountSyncManager(); + +module.exports = accountSyncManager; diff --git a/utils/animation-manager.js b/utils/animation-manager.js new file mode 100644 index 0000000..97a3d7d --- /dev/null +++ b/utils/animation-manager.js @@ -0,0 +1,564 @@ +// 动画管理器 - 微信小程序专用 +// 提供统一的动画效果和页面过渡管理 + +/** + * 动画管理器 + * 功能: + * 1. 页面过渡动画 + * 2. 组件动画效果 + * 3. 交互反馈动画 + * 4. 加载状态动画 + * 5. 手势动画 + * 6. 性能优化动画 + */ +class AnimationManager { + constructor() { + this.isInitialized = false; + + // 动画配置 + this.config = { + // 动画开关 + enabled: true, + + // 性能模式 + performanceMode: 'auto', // auto, high, low + + // 动画时长配置 + durations: { + fast: 200, + normal: 300, + slow: 500, + page: 400 + }, + + // 缓动函数配置 + easings: { + ease: 'ease', + easeIn: 'ease-in', + easeOut: 'ease-out', + easeInOut: 'ease-in-out', + linear: 'linear', + spring: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)', + bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)' + }, + + // 页面过渡配置 + pageTransitions: { + slideLeft: { + enter: { transform: 'translateX(100%)', opacity: 0 }, + active: { transform: 'translateX(0)', opacity: 1 }, + leave: { transform: 'translateX(-100%)', opacity: 0 } + }, + slideRight: { + enter: { transform: 'translateX(-100%)', opacity: 0 }, + active: { transform: 'translateX(0)', opacity: 1 }, + leave: { transform: 'translateX(100%)', opacity: 0 } + }, + slideUp: { + enter: { transform: 'translateY(100%)', opacity: 0 }, + active: { transform: 'translateY(0)', opacity: 1 }, + leave: { transform: 'translateY(-100%)', opacity: 0 } + }, + slideDown: { + enter: { transform: 'translateY(-100%)', opacity: 0 }, + active: { transform: 'translateY(0)', opacity: 1 }, + leave: { transform: 'translateY(100%)', opacity: 0 } + }, + fade: { + enter: { opacity: 0 }, + active: { opacity: 1 }, + leave: { opacity: 0 } + }, + scale: { + enter: { transform: 'scale(0.8)', opacity: 0 }, + active: { transform: 'scale(1)', opacity: 1 }, + leave: { transform: 'scale(1.2)', opacity: 0 } + } + } + }; + + // 动画实例缓存 + this.animationCache = new Map(); + + // 性能监控 + this.performanceStats = { + animationCount: 0, + averageDuration: 0, + droppedFrames: 0 + }; + + this.init(); + } + + // 初始化动画管理器 + init() { + if (this.isInitialized) return; + + console.log('🎭 初始化动画管理器...'); + + try { + // 检测设备性能 + this.detectPerformance(); + + // 设置全局动画样式 + this.setupGlobalStyles(); + + this.isInitialized = true; + console.log('✅ 动画管理器初始化完成'); + + } catch (error) { + console.error('❌ 动画管理器初始化失败:', error); + } + } + + // 🎭 ===== 基础动画 ===== + + // 创建动画实例 + createAnimation(options = {}) { + const defaultOptions = { + duration: this.config.durations.normal, + timingFunction: this.config.easings.easeOut, + delay: 0, + transformOrigin: '50% 50% 0' + }; + + const animationOptions = { ...defaultOptions, ...options }; + + // 根据性能模式调整动画 + if (this.config.performanceMode === 'low') { + animationOptions.duration = Math.min(animationOptions.duration, 200); + } + + const animation = wx.createAnimation(animationOptions); + + // 缓存动画实例 + const animationId = this.generateAnimationId(); + this.animationCache.set(animationId, { + animation: animation, + options: animationOptions, + createTime: Date.now() + }); + + return { animation, animationId }; + } + + // 淡入动画 + fadeIn(options = {}) { + const { animation } = this.createAnimation({ + duration: this.config.durations.normal, + timingFunction: this.config.easings.easeOut, + ...options + }); + + return animation.opacity(1).step(); + } + + // 淡出动画 + fadeOut(options = {}) { + const { animation } = this.createAnimation({ + duration: this.config.durations.normal, + timingFunction: this.config.easings.easeIn, + ...options + }); + + return animation.opacity(0).step(); + } + + // 滑入动画 + slideIn(direction = 'left', options = {}) { + const { animation } = this.createAnimation({ + duration: this.config.durations.normal, + timingFunction: this.config.easings.spring, + ...options + }); + + const transforms = { + left: () => animation.translateX(0), + right: () => animation.translateX(0), + up: () => animation.translateY(0), + down: () => animation.translateY(0) + }; + + return transforms[direction]().opacity(1).step(); + } + + // 滑出动画 + slideOut(direction = 'left', distance = '100%', options = {}) { + const { animation } = this.createAnimation({ + duration: this.config.durations.normal, + timingFunction: this.config.easings.easeIn, + ...options + }); + + const transforms = { + left: () => animation.translateX(`-${distance}`), + right: () => animation.translateX(distance), + up: () => animation.translateY(`-${distance}`), + down: () => animation.translateY(distance) + }; + + return transforms[direction]().opacity(0).step(); + } + + // 缩放动画 + scale(scale = 1, options = {}) { + const { animation } = this.createAnimation({ + duration: this.config.durations.normal, + timingFunction: this.config.easings.spring, + ...options + }); + + return animation.scale(scale).step(); + } + + // 旋转动画 + rotate(angle = 360, options = {}) { + const { animation } = this.createAnimation({ + duration: this.config.durations.slow, + timingFunction: this.config.easings.linear, + ...options + }); + + return animation.rotate(angle).step(); + } + + // 🎪 ===== 组合动画 ===== + + // 弹跳进入动画 + bounceIn(options = {}) { + const { animation } = this.createAnimation({ + duration: this.config.durations.slow, + timingFunction: this.config.easings.bounce, + ...options + }); + + return animation + .scale(0.3).opacity(0).step({ duration: 0 }) + .scale(1.05).opacity(1).step({ duration: 200 }) + .scale(0.95).step({ duration: 100 }) + .scale(1).step({ duration: 100 }); + } + + // 弹跳退出动画 + bounceOut(options = {}) { + const { animation } = this.createAnimation({ + duration: this.config.durations.normal, + timingFunction: this.config.easings.easeIn, + ...options + }); + + return animation + .scale(1.1).step({ duration: 100 }) + .scale(0).opacity(0).step({ duration: 200 }); + } + + // 摇摆动画 + shake(options = {}) { + const { animation } = this.createAnimation({ + duration: this.config.durations.fast, + timingFunction: this.config.easings.linear, + ...options + }); + + return animation + .translateX(-10).step({ duration: 50 }) + .translateX(10).step({ duration: 50 }) + .translateX(-10).step({ duration: 50 }) + .translateX(10).step({ duration: 50 }) + .translateX(0).step({ duration: 50 }); + } + + // 脉冲动画 + pulse(options = {}) { + const { animation } = this.createAnimation({ + duration: this.config.durations.normal, + timingFunction: this.config.easings.easeInOut, + ...options + }); + + return animation + .scale(1).step({ duration: 0 }) + .scale(1.05).step({ duration: 150 }) + .scale(1).step({ duration: 150 }); + } + + // 🎬 ===== 页面过渡 ===== + + // 页面进入动画 + pageEnter(transitionType = 'slideLeft', options = {}) { + const transition = this.config.pageTransitions[transitionType]; + if (!transition) { + console.warn('未知的页面过渡类型:', transitionType); + return this.fadeIn(options); + } + + const { animation } = this.createAnimation({ + duration: this.config.durations.page, + timingFunction: this.config.easings.easeOut, + ...options + }); + + // 设置初始状态 + this.applyTransform(animation, transition.enter); + animation.step({ duration: 0 }); + + // 执行进入动画 + this.applyTransform(animation, transition.active); + return animation.step(); + } + + // 页面退出动画 + pageLeave(transitionType = 'slideLeft', options = {}) { + const transition = this.config.pageTransitions[transitionType]; + if (!transition) { + console.warn('未知的页面过渡类型:', transitionType); + return this.fadeOut(options); + } + + const { animation } = this.createAnimation({ + duration: this.config.durations.page, + timingFunction: this.config.easings.easeIn, + ...options + }); + + // 执行退出动画 + this.applyTransform(animation, transition.leave); + return animation.step(); + } + + // 🎯 ===== 交互动画 ===== + + // 按钮点击动画 + buttonPress(options = {}) { + const { animation } = this.createAnimation({ + duration: this.config.durations.fast, + timingFunction: this.config.easings.easeOut, + ...options + }); + + return animation + .scale(0.95).step({ duration: 100 }) + .scale(1).step({ duration: 100 }); + } + + // 卡片悬停动画 + cardHover(options = {}) { + const { animation } = this.createAnimation({ + duration: this.config.durations.normal, + timingFunction: this.config.easings.easeOut, + ...options + }); + + return animation + .scale(1.02) + .translateY(-2) + .step(); + } + + // 列表项滑动动画 + listItemSlide(direction = 'left', options = {}) { + const { animation } = this.createAnimation({ + duration: this.config.durations.normal, + timingFunction: this.config.easings.easeOut, + ...options + }); + + const distance = direction === 'left' ? -80 : 80; + return animation.translateX(distance).step(); + } + + // 🔄 ===== 加载动画 ===== + + // 旋转加载动画 + loadingSpinner(options = {}) { + const { animation } = this.createAnimation({ + duration: 1000, + timingFunction: this.config.easings.linear, + ...options + }); + + return animation.rotate(360).step(); + } + + // 脉冲加载动画 + loadingPulse(options = {}) { + const { animation } = this.createAnimation({ + duration: 1000, + timingFunction: this.config.easings.easeInOut, + ...options + }); + + return animation + .opacity(0.3).step({ duration: 500 }) + .opacity(1).step({ duration: 500 }); + } + + // 波浪加载动画 + loadingWave(delay = 0, options = {}) { + const { animation } = this.createAnimation({ + duration: 600, + timingFunction: this.config.easings.easeInOut, + delay: delay, + ...options + }); + + return animation + .translateY(-10).step({ duration: 300 }) + .translateY(0).step({ duration: 300 }); + } + + // 🎨 ===== 工具方法 ===== + + // 应用变换 + applyTransform(animation, transform) { + Object.keys(transform).forEach(property => { + const value = transform[property]; + + switch (property) { + case 'opacity': + animation.opacity(parseFloat(value)); + break; + case 'transform': + // 解析transform值 + this.parseTransform(animation, value); + break; + default: + console.warn('不支持的动画属性:', property); + } + }); + } + + // 解析transform值 + parseTransform(animation, transformValue) { + const transforms = transformValue.split(' '); + + transforms.forEach(transform => { + if (transform.includes('translateX')) { + const value = this.extractValue(transform); + animation.translateX(value); + } else if (transform.includes('translateY')) { + const value = this.extractValue(transform); + animation.translateY(value); + } else if (transform.includes('scale')) { + const value = parseFloat(this.extractValue(transform)); + animation.scale(value); + } else if (transform.includes('rotate')) { + const value = this.extractValue(transform); + animation.rotate(parseFloat(value)); + } + }); + } + + // 提取变换值 + extractValue(transform) { + const match = transform.match(/\(([^)]+)\)/); + return match ? match[1] : '0'; + } + + // 检测设备性能 + detectPerformance() { + try { + // 使用新的API替代已弃用的wx.getSystemInfoSync + const deviceInfo = wx.getDeviceInfo(); + const { platform, system, model } = deviceInfo; + + // 简单的性能评估 + let performanceScore = 100; + + // 根据平台调整 + if (platform === 'android') { + performanceScore -= 10; + } + + // 根据系统版本调整 + const systemVersion = parseFloat(system.match(/[\d.]+/)?.[0] || '0'); + if (systemVersion < 10) { + performanceScore -= 20; + } + + // 根据机型调整(简化判断) + if (model && model.toLowerCase().includes('redmi')) { + performanceScore -= 15; + } + + // 设置性能模式 + if (performanceScore >= 80) { + this.config.performanceMode = 'high'; + } else if (performanceScore >= 60) { + this.config.performanceMode = 'auto'; + } else { + this.config.performanceMode = 'low'; + // 低性能模式下禁用复杂动画 + this.config.durations.normal = 200; + this.config.durations.slow = 300; + } + + console.log('🎭 设备性能评估:', { + score: performanceScore, + mode: this.config.performanceMode + }); + + } catch (error) { + console.error('❌ 性能检测失败:', error); + this.config.performanceMode = 'auto'; + } + } + + // 设置全局动画样式 + setupGlobalStyles() { + // 这里可以设置一些全局的CSS动画类 + // 微信小程序中主要通过WXSS实现 + } + + // 生成动画ID + generateAnimationId() { + return `anim_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + // 清理动画缓存 + cleanupAnimationCache() { + const now = Date.now(); + const expireTime = 5 * 60 * 1000; // 5分钟 + + for (const [id, cache] of this.animationCache) { + if (now - cache.createTime > expireTime) { + this.animationCache.delete(id); + } + } + } + + // 获取性能统计 + getPerformanceStats() { + return { + ...this.performanceStats, + cacheSize: this.animationCache.size, + performanceMode: this.config.performanceMode + }; + } + + // 设置动画开关 + setEnabled(enabled) { + this.config.enabled = enabled; + console.log('🎭 动画开关:', enabled ? '开启' : '关闭'); + } + + // 设置性能模式 + setPerformanceMode(mode) { + if (['auto', 'high', 'low'].includes(mode)) { + this.config.performanceMode = mode; + console.log('🎭 性能模式:', mode); + } + } + + // 销毁动画管理器 + destroy() { + this.animationCache.clear(); + this.isInitialized = false; + console.log('🎭 动画管理器已销毁'); + } +} + +// 创建全局实例 +const animationManager = new AnimationManager(); + +module.exports = animationManager; diff --git a/utils/api-client.js b/utils/api-client.js new file mode 100644 index 0000000..275b2b2 --- /dev/null +++ b/utils/api-client.js @@ -0,0 +1,1091 @@ +// API客户端工具类 - 真实API版本 +const config = require('../config/config.js'); + +class ApiClient { + constructor() { + this.baseUrl = config.api.baseUrl; + this.timeout = config.api.timeout || 15000; + this.token = null; + + // 自动从本地存储获取token + try { + const userInfo = wx.getStorageSync('userInfo'); + if (userInfo && userInfo.token) { + this.token = userInfo.token; + console.log('API客户端初始化,已加载token'); + } + } catch (error) { + console.error('初始化时获取token失败:', error); + } + + // 请求拦截器 + this.requestInterceptors = []; + this.responseInterceptors = []; + + console.log('API客户端初始化:', { + baseUrl: this.baseUrl, + timeout: this.timeout, + configLoaded: !!config?.api + }); + } + + // 设置token + setToken(token) { + this.token = token; + console.log('Token已设置'); + } + + // 清除token + clearToken() { + this.token = null; + console.log('Token已清除'); + } + + // 获取token + getToken() { + // 如果没有token,尝试从存储中获取 + if (!this.token) { + try { + const userInfo = wx.getStorageSync('userInfo'); + if (userInfo && userInfo.token) { + this.token = userInfo.token; + console.log('从存储中获取token成功,用户customId:', userInfo.user?.customId); + } else { + console.log('存储中没有找到有效的token'); + } + } catch (error) { + console.error('从存储获取token失败:', error); + } + } + return this.token; + } + + // 获取设备信息 + async getDeviceInfo() { + try { + // 使用新的API替代废弃的wx.getSystemInfoSync + const [windowInfo, deviceInfo, appBaseInfo] = await Promise.all([ + this.getWindowInfo(), + this.getDeviceInfo_new(), + this.getAppBaseInfo() + ]); + + const systemInfo = { ...windowInfo, ...deviceInfo, ...appBaseInfo }; + + return { + deviceId: deviceInfo.deviceId || systemInfo.deviceId || 'unknown', + deviceModel: deviceInfo.model || systemInfo.model || 'unknown', + deviceType: 'miniprogram', // 标识为小程序 + appVersion: config?.appVersion || '1.0.0', + platform: deviceInfo.platform || systemInfo.platform || 'unknown', + system: deviceInfo.system || systemInfo.system || 'unknown' + }; + } catch (error) { + console.error('获取设备信息失败,使用兜底方案:', error); + // 兜底使用旧API + try { + const systemInfo = wx.getSystemInfoSync(); + return { + deviceId: systemInfo.deviceId || 'unknown', + deviceModel: systemInfo.model || 'unknown', + deviceType: 'miniprogram', + appVersion: config?.appVersion || '1.0.0', + platform: systemInfo.platform || 'unknown', + system: systemInfo.system || 'unknown' + }; + } catch (fallbackError) { + console.error('兜底方案也失败:', fallbackError); + return { + deviceId: 'unknown', + deviceModel: 'unknown', + deviceType: 'miniprogram', + appVersion: config?.appVersion || '1.0.0', + platform: 'unknown', + system: 'unknown' + }; + } + } + } + + // 获取窗口信息 + getWindowInfo() { + return new Promise((resolve) => { + try { + const windowInfo = wx.getWindowInfo(); + resolve(windowInfo); + } catch (error) { + resolve({}); + } + }); + } + + // 获取设备信息(新API) + getDeviceInfo_new() { + return new Promise((resolve) => { + try { + const deviceInfo = wx.getDeviceInfo(); + resolve(deviceInfo); + } catch (error) { + resolve({}); + } + }); + } + + // 获取应用基础信息 + getAppBaseInfo() { + return new Promise((resolve) => { + try { + const appBaseInfo = wx.getAppBaseInfo(); + resolve(appBaseInfo); + } catch (error) { + resolve({}); + } + }); + } + + // 编码查询参数 + encodeParams(params) { + if (!params) return ''; + + return Object.keys(params) + .filter(key => params[key] !== null && params[key] !== undefined) + .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) + .join('&'); + } + + // 通用请求方法 + async request(method, url, data = null, options = {}) { + const token = this.getToken(); + const fullUrl = url.startsWith('http') ? url : `${this.baseUrl}${url}`; + + console.log('发起API请求:', { + method, + url: fullUrl, + hasToken: !!token, + hasData: !!data + }); + + const requestOptions = { + url: fullUrl, + method: method.toUpperCase(), + timeout: this.timeout, + header: { + 'Content-Type': 'application/json', + 'X-Client-Version': `FindMe-MiniProgram/${config?.appVersion || '1.0.0'}`, + ...options.headers + } + }; + + // 添加认证头 + if (token) { + requestOptions.header['Authorization'] = `Bearer ${token}`; + } + + // 处理请求数据 + if (data) { + if (method.toUpperCase() === 'GET') { + // GET请求,将数据转换为查询参数 + const queryString = this.encodeParams(data); + if (queryString) { + requestOptions.url += (requestOptions.url.includes('?') ? '&' : '?') + queryString; + } + } else { + // 其他请求,将数据作为请求体 + requestOptions.data = data; + } + } + + return new Promise((resolve, reject) => { + wx.request({ + ...requestOptions, + success: (res) => { + console.log('API响应:', { + url: fullUrl, + statusCode: res.statusCode, + data: res.data + }); + + if (res.statusCode >= 200 && res.statusCode < 300) { + // 检查业务状态码 + if (res.data && typeof res.data === 'object') { + if (res.data.code === 0 || res.data.code === 200) { + resolve(res.data); + } else { + reject(new Error(`HTTP ${res.statusCode}: ${res.data.message || 'request:ok'}`)); + } + } else { + resolve(res.data); + } + } else { + reject(new Error(`HTTP ${res.statusCode}: ${res.data?.message || 'request failed'}`)); + } + }, + fail: (error) => { + console.error('API请求失败:', error); + reject(new Error(error.errMsg || '网络请求失败')); + } + }); + }); + } + + // GET请求 + async get(url, params = null, options = {}) { + return this.request('GET', url, params, options); + } + + // POST请求 + async post(url, data = null, options = {}) { + return this.request('POST', url, data, options); + } + + // PUT请求 + async put(url, data = null, options = {}) { + return this.request('PUT', url, data, options); + } + + // DELETE请求 + async delete(url, data = null, options = {}) { + return this.request('DELETE', url, data, options); + } + + // 🔥 用户认证相关接口 + + // 发送验证码(生产级别实现,支持重试) + async sendVerifyCode(phone, retryCount = 0) { + try { + const response = await this.post('/api/v1/user/send-verify-code', { phone }); + return response; + } catch (error) { + console.error('发送验证码失败:', error); + + // 网络错误时自动重试(最多重试2次) + if (retryCount < 2 && this.isNetworkError(error)) { + console.log(`网络错误,正在重试... (${retryCount + 1}/2)`); + await this.delay(1000 * (retryCount + 1)); // 递增延迟 + return this.sendVerifyCode(phone, retryCount + 1); + } + + throw error; + } + } + + // 判断是否为网络错误 + isNetworkError(error) { + return error.message && ( + error.message.includes('网络') || + error.message.includes('timeout') || + error.message.includes('Network') || + error.message.includes('Failed to fetch') + ); + } + + // 延迟函数 + delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + // 用户登录 + async login(phone, verifyCode) { + try { + // 获取设备信息 + const deviceInfo = await this.getDeviceInfo(); + + const response = await this.post('/api/v1/user/login', { + phone, + verifyCode, + source: 'miniprogram', // 根据文档使用source字段(小写) + deviceId: deviceInfo.deviceId, + deviceType: deviceInfo.deviceType, + appVersion: deviceInfo.appVersion + }); + + // 不在这里保存数据,让认证管理器统一处理 + console.log('登录API调用成功,设备信息:', deviceInfo); + return response; + } catch (error) { + console.error('登录失败:', error); + throw error; + } + } + + // 微信登录 + async wechatLogin(code, userInfo = null) { + try { + // 获取设备信息 + const deviceInfo = await this.getDeviceInfo(); + + const loginData = { + code, + source: 'miniprogram', // 根据文档使用source字段(小写) + deviceId: deviceInfo.deviceId, + deviceType: deviceInfo.deviceType, + appVersion: deviceInfo.appVersion + }; + + if (userInfo) { + loginData.userInfo = userInfo; + } + + const response = await this.post('/api/v1/user/wechat-login', loginData); + + // 不在这里保存数据,让认证管理器统一处理 + console.log('微信登录API调用成功,设备信息:', deviceInfo); + return response; + } catch (error) { + console.error('微信登录失败:', error); + throw error; + } + } + + // 获取用户信息 + async getUserInfo() { + try { + const response = await this.get('/api/v1/user/info'); + return response; + } catch (error) { + console.error('获取用户信息失败:', error); + throw error; + } + } + + // 🔥 账号同步相关接口 + + // 绑定手机号 + async bindPhone(phone, verifyCode, autoMerge = false) { + try { + // 获取设备信息 + const deviceInfo = await this.getDeviceInfo(); + + const response = await this.post('/api/v1/user/bind-phone', { + phone, + verifyCode, + deviceId: deviceInfo.deviceId, // 根据文档保留deviceId + autoMerge + }); + return response; + } catch (error) { + console.error('绑定手机号失败:', error); + throw error; + } + } + + // 检测可合并账号 + async detectMerge(customId, autoMerge = false) { + try { + const response = await this.post('/api/v1/user/detect-merge', { + userCustomId: customId, // 根据文档使用userCustomId参数 + autoMerge + }); + return response; + } catch (error) { + console.error('检测可合并账号失败:', error); + throw error; + } + } + + // 合并账号 + async mergeAccount(primaryCustomId, secondaryCustomId, mergeReason = '用户手动合并') { + try { + const response = await this.post('/api/v1/user/merge-account', { + primaryUserCustomId: primaryCustomId, // 根据文档使用primaryUserCustomId参数 + secondaryUserCustomId: secondaryCustomId, // 根据文档使用secondaryUserCustomId参数 + mergeReason + }); + return response; + } catch (error) { + console.error('合并账号失败:', error); + throw error; + } + } + + // 更新用户资料 + async updateUserProfile(data) { + try { + const response = await this.put('/api/v1/user/profile', data); + return response; + } catch (error) { + console.error('更新用户资料失败:', error); + throw error; + } + } + + // 获取用户设置 + async getUserSetting() { + try { + const response = await this.get('/api/v1/user/setting'); + return response; + } catch (error) { + console.error('获取用户设置失败:', error); + throw error; + } + } + + // 刷新token + async refreshToken(refreshToken) { + try { + const response = await this.post('/api/v1/auth/refresh', { + refresh_token: refreshToken + }); + + if (response && response.code === 200 && response.data) { + this.setToken(response.data.access_token); + + // 更新本地存储 - 保持字段名一致性 + const userInfo = wx.getStorageSync('userInfo') || {}; + userInfo.token = response.data.access_token; + userInfo.refreshToken = response.data.refresh_token; // 保持一致的字段名 + userInfo.expiresAt = response.data.expires_at * 1000; // 转换为毫秒时间戳 + wx.setStorageSync('userInfo', userInfo); + } + + return response; + } catch (error) { + console.error('刷新token失败:', error); + throw error; + } + } + + // 用户登出 + async logout() { + try { + const response = await this.post('/api/v1/user/logout'); + + // 清除本地token和用户信息 + this.clearToken(); + wx.removeStorageSync('userInfo'); + + return response; + } catch (error) { + console.error('登出失败:', error); + // 即使登出失败,也清除本地信息 + this.clearToken(); + wx.removeStorageSync('userInfo'); + throw error; + } + } + + // 🔥 位置相关接口 + + // 更新位置 + async updateLocation(locationData) { + try { + const response = await this.post('/api/v1/location/update', locationData); + return response; + } catch (error) { + console.error('更新位置失败:', error); + throw error; + } + } + + // 获取好友位置 + async getFriendsLocation() { + try { + const response = await this.get('/api/v1/location/friends'); + return response; + } catch (error) { + console.error('获取好友位置失败:', error); + throw error; + } + } + + // 获取用户位置 + async getUserLocation(userId) { + try { + const response = await this.get(`/api/v1/location/user/${userId}`); + return response; + } catch (error) { + console.error('获取用户位置失败:', error); + throw error; + } + } + + // 获取附近用户 + async getNearbyUsers(params = {}) { + try { + const response = await this.get('/api/v1/location/nearby', params); + return response; + } catch (error) { + console.error('获取附近用户失败:', error); + throw error; + } + } + + // 获取位置历史 + async getLocationHistory(params = {}) { + try { + const response = await this.get('/api/v1/location/history', params); + return response; + } catch (error) { + console.error('获取位置历史失败:', error); + throw error; + } + } + + // 获取位置隐私设置 + async getLocationPrivacy() { + try { + const response = await this.get('/api/v1/location/privacy'); + return response; + } catch (error) { + console.error('获取位置隐私设置失败:', error); + throw error; + } + } + + // 更新位置隐私设置 + async updateLocationPrivacy(privacyData) { + try { + const response = await this.put('/api/v1/location/privacy', privacyData); + return response; + } catch (error) { + console.error('更新位置隐私设置失败:', error); + throw error; + } + } + + // 获取天气信息 + async getWeatherInfo(latitude, longitude) { + try { + const response = await this.get('/api/v1/location/weather', { + latitude, + longitude + }); + return response; + } catch (error) { + console.error('获取天气信息失败:', error); + throw error; + } + } + + // 🔥 社交相关接口(根据好友功能API手册完整实现) + + // 获取好友列表 + async getFriends() { + try { + const response = await this.get('/api/v1/social/friends'); + return response; + } catch (error) { + console.error('获取好友列表失败:', error); + throw error; + } + } + + // 获取好友详细信息 + async getFriendDetail(customId, lat = null, lng = null) { + try { + let url = `/api/v1/social/friends/${customId}/detail`; + const params = {}; + if (lat !== null && lng !== null) { + params.lat = lat; + params.lng = lng; + } + + const response = await this.get(url, params); + return response; + } catch (error) { + console.error('获取好友详情失败:', error); + throw error; + } + } + + // 搜索用户 + async searchUsers(query, searchType = 'all', page = 1, pageSize = 10) { + try { + if (!query || typeof query !== 'string') { + throw new Error('搜索关键词不能为空'); + } + + const validSearchTypes = ['nickname', 'custom_id', 'phone', 'all']; + if (!validSearchTypes.includes(searchType)) { + throw new Error('无效的搜索类型'); + } + + if (pageSize < 1 || pageSize > 50) { + throw new Error('每页数量必须在1-50之间'); + } + + const response = await this.post('/api/v1/social/users/search', { + query: query.trim(), + searchType: searchType, + page: Math.max(1, page), + pageSize: Math.min(50, pageSize) + }); + return response; + } catch (error) { + console.error('搜索用户失败:', error); + throw error; + } + } + + // 添加好友 + async addFriend(targetId, message = '') { + try { + if (!targetId || typeof targetId !== 'string') { + throw new Error('目标用户ID不能为空'); + } + + if (message && message.length > 100) { + throw new Error('好友申请留言不能超过100字符'); + } + + const response = await this.post('/api/v1/social/friend/add', { + targetId: targetId, + message: message.trim() + }); + return response; + } catch (error) { + console.error('添加好友失败:', error); + throw error; + } + } + + // 获取好友请求列表 + async getFriendRequests() { + try { + const response = await this.get('/api/v1/social/friend/requests'); + return response; + } catch (error) { + console.error('获取好友请求失败:', error); + throw error; + } + } + + // 获取好友请求数量 + async getFriendRequestsCount() { + try { + const response = await this.get('/api/v1/social/friend/requests/count'); + return response; + } catch (error) { + console.error('获取好友请求数量失败:', error); + throw error; + } + } + + // 处理好友请求 + async handleFriendRequest(requestId, accept) { + try { + if (!requestId || typeof requestId !== 'string') { + throw new Error('请求ID不能为空'); + } + + if (typeof accept !== 'boolean') { + throw new Error('accept参数必须是布尔值'); + } + + const response = await this.post('/api/v1/social/friend/handle-request', { + requestId: requestId, + accept: accept + }); + return response; + } catch (error) { + console.error('处理好友请求失败:', error); + throw error; + } + } + + // 批量处理好友请求 + async batchHandleFriendRequests(requestIds, accept) { + try { + if (!Array.isArray(requestIds) || requestIds.length === 0) { + throw new Error('请求ID列表不能为空'); + } + + if (requestIds.length > 20) { + throw new Error('一次最多处理20个请求'); + } + + if (typeof accept !== 'boolean') { + throw new Error('accept参数必须是布尔值'); + } + + const response = await this.post('/api/v1/social/friend/batch-handle-requests', { + requestIds: requestIds, + accept: accept + }); + return response; + } catch (error) { + console.error('批量处理好友请求失败:', error); + throw error; + } + } + + // 更新好友关系 + async updateFriendRelation(friendId, updateData) { + try { + if (!friendId) { + throw new Error('好友ID不能为空'); + } + + const validRelations = ['情侣', '家人', '兄弟', '姐妹', '闺蜜', '死党']; + if (updateData.relation && !validRelations.includes(updateData.relation)) { + throw new Error('无效的关系标签'); + } + + if (updateData.remark && updateData.remark.length > 50) { + throw new Error('好友备注不能超过50字符'); + } + + const response = await this.put('/api/v1/social/friend', { + friendId: friendId, + ...updateData + }); + return response; + } catch (error) { + console.error('更新好友关系失败:', error); + throw error; + } + } + + // 删除好友 + async deleteFriend(friendCustomId) { + try { + if (!friendCustomId || typeof friendCustomId !== 'string') { + throw new Error('好友CustomID不能为空'); + } + + const response = await this.delete(`/api/v1/social/friend/${friendCustomId}`); + return response; + } catch (error) { + console.error('删除好友失败:', error); + throw error; + } + } + + // 拉取好友通知 + async pullFriendNotifications(limit = 10) { + try { + if (limit < 1 || limit > 50) { + throw new Error('拉取数量必须在1-50之间'); + } + + const response = await this.get('/api/v1/social/friend/notifications/pull', { + limit: limit + }); + return response; + } catch (error) { + console.error('拉取好友通知失败:', error); + throw error; + } + } + + // 获取通知统计 + async getFriendNotificationStats() { + try { + const response = await this.get('/api/v1/social/friend/notifications/stats'); + return response; + } catch (error) { + console.error('获取通知统计失败:', error); + throw error; + } + } + + // 获取群组列表 + async getGroups() { + try { + const response = await this.get('/api/v1/social/groups'); + return response; + } catch (error) { + console.error('获取群组列表失败:', error); + throw error; + } + } + + // 获取群组数量 + async getGroupsCount() { + try { + const response = await this.get('/api/v1/social/groups/count'); + return response; + } catch (error) { + console.error('获取群组数量失败:', error); + throw error; + } + } + + // 🔥 聊天相关接口(根据WebSocket即时通讯接口文档完整实现) + + // 获取会话列表 + async getConversations() { + try { + const response = await this.get('/api/v1/chat/conversations'); + return response; + } catch (error) { + console.error('获取会话列表失败:', error); + throw error; + } + } + + // 获取聊天历史消息 - 根据API文档修正参数格式 + async getChatMessages(targetId, chatType, params = {}) { + try { + const queryParams = { + receiverId: targetId, // 使用receiverId而不是conversationId + chatType: chatType, // 聊天类型:0=单聊, 1=群聊 + limit: params.limit || 20, + direction: params.direction || 'before', + lastMsgId: params.lastMsgId, // 分页参数 + ...params + }; + + const response = await this.get('/api/v1/chat/history', queryParams); + return response; + } catch (error) { + console.error('获取聊天消息失败:', error); + throw error; + } + } + + // 🚫 发送消息已废弃 - 必须使用WebSocket + async sendMessage(targetId, content, msgType = 'text', chatType = 0) { + console.error('❌ HTTP发送消息已废弃!根据API文档,所有消息发送必须通过WebSocket'); + throw new Error('消息发送必须使用WebSocket,HTTP发送接口已废弃'); + } + + // 批量标记消息已读 + async batchMarkRead(conversationId, messageIds) { + try { + const response = await this.post('/api/v1/chat/batch-read', { + conversationId: conversationId, + messageIds: messageIds + }); + return response; + } catch (error) { + console.error('批量标记已读失败:', error); + throw error; + } + } + + // 全部标记已读 + async markAllRead(conversationId) { + try { + const response = await this.post('/api/v1/chat/mark-all-read', { + conversationId: conversationId + }); + return response; + } catch (error) { + console.error('全部标记已读失败:', error); + throw error; + } + } + + // 获取总未读数 + async getTotalUnreadCount() { + try { + const response = await this.get('/api/v1/chat/unread/total'); + return response; + } catch (error) { + console.error('获取总未读数失败:', error); + throw error; + } + } + + // 更新会话设置 + async updateConversationSettings(conversationId, settings) { + try { + const response = await this.put(`/api/v1/chat/conversation/${conversationId}`, settings); + return response; + } catch (error) { + console.error('更新会话设置失败:', error); + throw error; + } + } + + // 删除会话 + async deleteConversation(conversationId) { + try { + const response = await this.delete(`/api/v1/chat/conversation/${conversationId}`); + return response; + } catch (error) { + console.error('删除会话失败:', error); + throw error; + } + } + + // 获取聊天设置 + async getChatSettings() { + try { + const response = await this.get('/api/v1/chat/settings'); + return response; + } catch (error) { + console.error('获取聊天设置失败:', error); + throw error; + } + } + + // 更新聊天设置 + async updateChatSettings(settings) { + try { + const response = await this.put('/api/v1/chat/settings', settings); + return response; + } catch (error) { + console.error('更新聊天设置失败:', error); + throw error; + } + } + + // 拉取离线消息 + async pullOfflineMessages(lastSeqId, limit = 50) { + try { + const response = await this.get('/api/v1/chat/sync/pull', { + lastSeqId: lastSeqId, + limit: limit + }); + return response; + } catch (error) { + console.error('拉取离线消息失败:', error); + throw error; + } + } + + // 确认消息状态 + async ackMessageStatus(messageId, status, timestamp) { + try { + const response = await this.post('/api/v1/chat/sync/ack', { + messageId: messageId, + status: status, + timestamp: timestamp + }); + return response; + } catch (error) { + console.error('确认消息状态失败:', error); + throw error; + } + } + + // 标记消息已读(兼容方法) + async markMessagesRead(conversationId, messageIds) { + try { + if (Array.isArray(messageIds) && messageIds.length > 0) { + return await this.batchMarkRead(conversationId, messageIds); + } else { + return await this.markAllRead(conversationId); + } + } catch (error) { + console.error('标记消息已读失败:', error); + throw error; + } + } + + // 发送虚拟弹幕 + async sendDanmaku(content, color = '#FFFFFF', size = 1, duration = 5000, latitude, longitude, radius = 100) { + try { + if (!content || content.length > 255) { + throw new Error('弹幕内容不能为空且不能超过255字符'); + } + + const response = await this.post('/api/v1/chat/danmaku', { + content: content, + color: color, + size: size, + duration: duration, + latitude: latitude, + longitude: longitude, + radius: radius + }); + return response; + } catch (error) { + console.error('发送弹幕失败:', error); + throw error; + } + } + + // 查询附近弹幕 + async getNearbyDanmaku(latitude, longitude, radius = 1000) { + try { + const response = await this.get('/api/v1/chat/danmaku/nearby', { + latitude: latitude, + longitude: longitude, + radius: radius + }); + return response; + } catch (error) { + console.error('查询附近弹幕失败:', error); + throw error; + } + } + + // 查询表情包 + async getEmojiPackages() { + try { + const response = await this.get('/api/v1/chat/emoji/packages'); + return response; + } catch (error) { + console.error('查询表情包失败:', error); + throw error; + } + } + + // 🔥 文件上传接口 + + // 文件上传 + async uploadFile(filePath, fileType = 'image', usageType = 'chat') { + try { + console.log('上传文件:', { filePath, fileType, usageType }); + + return new Promise((resolve, reject) => { + wx.uploadFile({ + url: `${this.baseUrl}/api/v1/file/upload`, + filePath: filePath, + name: 'file', + formData: { + file_type: fileType, + usage_type: usageType + }, + header: { + 'Authorization': this.token ? `Bearer ${this.token}` : '', + 'X-Client-Version': `FindMe-MiniProgram/${config?.appVersion || '1.0.0'}` + }, + success: (res) => { + console.log('文件上传成功:', res); + try { + const data = JSON.parse(res.data); + if (data.code === 0) { + resolve(data); + } else { + reject(new Error(data.message || '上传失败')); + } + } catch (error) { + reject(new Error('响应解析失败')); + } + }, + fail: (error) => { + console.error('文件上传失败:', error); + reject(new Error(error.errMsg || '上传失败')); + } + }); + }); + } catch (error) { + console.error('文件上传异常:', error); + throw error; + } + } + + // 🔥 测试token有效性 + async testTokenValidity() { + try { + const token = this.getToken(); + + console.log('开始测试token有效性:', { + hasToken: !!token, + tokenLength: token ? token.length : 0, + tokenStart: token ? token.substring(0, 20) + '...' : 'null' + }); + + if (!token) { + throw new Error('没有找到token'); + } + + const response = await this.getUserInfo(); + console.log('Token有效性测试成功:', response); + return true; + + } catch (error) { + console.error('Token有效性测试失败:', error); + return false; + } + } +} + +// 创建全局实例 +const apiClient = new ApiClient(); + +module.exports = apiClient; \ No newline at end of file diff --git a/utils/auth.js b/utils/auth.js new file mode 100644 index 0000000..13bac5d --- /dev/null +++ b/utils/auth.js @@ -0,0 +1,446 @@ +// 认证工具类 - 参考Flutter的auth_provider.dart +const apiClient = require('./api-client.js'); + +class AuthManager { + constructor() { + this.isInitialized = false; + this.loginPromise = null; // 防止并发登录 + this.tokenRefreshPromise = null; // 防止并发刷新token + this._app = null; // 缓存app实例 + } + + // 安全获取app实例 + getApp() { + if (!this._app) { + try { + this._app = getApp(); + } catch (error) { + console.error('获取app实例失败:', error); + return null; + } + } + return this._app; + } + + // 初始化认证管理器 + async init() { + if (this.isInitialized) { + return; + } + + console.log('初始化认证管理器'); + + try { + // 检查本地存储的用户信息 + const userInfo = wx.getStorageSync('userInfo'); + if (userInfo && userInfo.token) { + console.log('发现本地用户信息,尝试恢复登录状态'); + + // 检查token是否过期 + if (this.isTokenValid(userInfo)) { + // 恢复登录状态 + const app = this.getApp(); + if (app) { + app.globalData.userInfo = userInfo; + app.globalData.isLoggedIn = true; + } + apiClient.setToken(userInfo.token); + + console.log('登录状态恢复成功'); + } else { + console.log('Token已过期,尝试刷新'); + await this.refreshTokenIfNeeded(userInfo); + } + } else { + console.log('未发现本地登录信息'); + } + + this.isInitialized = true; + console.log('认证管理器初始化完成'); + + } catch (error) { + console.error('认证管理器初始化失败:', error); + this.clearAuthData(); + this.isInitialized = true; + } + } + + // 检查token是否有效 + isTokenValid(userInfo) { + if (!userInfo || !userInfo.token || !userInfo.expiresAt) { + return false; + } + + // 检查是否过期(提前5分钟判断过期) + const expiresAt = new Date(userInfo.expiresAt).getTime(); + const now = Date.now(); + const bufferTime = 5 * 60 * 1000; // 5分钟缓冲时间 + + return (expiresAt - now) > bufferTime; + } + + // 检查是否已登录 + isLoggedIn() { + const app = this.getApp(); + return app?.globalData?.isLoggedIn && !!app?.globalData?.userInfo?.token; + } + + // 获取当前用户信息 + getCurrentUser() { + const app = this.getApp(); + return app?.globalData?.userInfo?.user || null; + } + + // 获取完整的认证信息(包含token) + getFullUserInfo() { + const app = this.getApp(); + return app?.globalData?.userInfo || null; + } + + // 获取当前token + getCurrentToken() { + const app = this.getApp(); + return app?.globalData?.userInfo?.token || null; + } + + // 静默登录检查 + async ensureAuthenticated() { + if (!this.isInitialized) { + await this.init(); + } + + if (this.isLoggedIn()) { + console.log('用户已登录'); + return true; + } + + console.log('用户未登录,需要跳转到登录页'); + return false; + } + + // 刷新token + async refreshTokenIfNeeded(userInfo) { + // 防止并发刷新 + if (this.tokenRefreshPromise) { + return await this.tokenRefreshPromise; + } + + if (!userInfo?.refreshToken) { + console.log('没有refresh token,无法刷新'); + this.clearAuthData(); + return false; + } + + console.log('开始刷新token'); + + this.tokenRefreshPromise = this._doRefreshToken(userInfo.refreshToken); + + try { + const result = await this.tokenRefreshPromise; + return result; + } finally { + this.tokenRefreshPromise = null; + } + } + + // 执行token刷新 + async _doRefreshToken(refreshToken) { + try { + const response = await apiClient.refreshToken(refreshToken); + + if (response && response.data) { + // 更新用户信息 + const app = this.getApp(); + const currentUserInfo = app?.globalData?.userInfo || {}; + const updatedUserInfo = { + ...currentUserInfo, + token: response.data.access_token, + refreshToken: response.data.refresh_token || refreshToken, + expiresAt: response.data.expires_at * 1000, // 转换为毫秒时间戳 + loginTime: Date.now() + }; + + // 保存到本地存储 + wx.setStorageSync('userInfo', updatedUserInfo); + + // 更新全局状态 + if (app) { + app.globalData.userInfo = updatedUserInfo; + app.globalData.isLoggedIn = true; + } + + // 更新API客户端token + apiClient.setToken(response.data.access_token); + + console.log('Token刷新成功'); + return true; + } else { + throw new Error('刷新token响应格式错误'); + } + + } catch (error) { + console.error('Token刷新失败:', error); + this.clearAuthData(); + return false; + } + } + + // 登录成功后保存认证信息 + async saveAuthData(loginData) { + try { + console.log('开始保存认证信息:', { + hasAccessToken: !!loginData.access_token, + hasRefreshToken: !!loginData.refresh_token, + hasUser: !!loginData.user, + userCustomId: loginData.user?.customId, + expiresAt: loginData.expires_at + }); + + // 先清除旧的认证数据 + this.clearAuthData(); + + const userInfo = { + token: loginData.access_token, + refreshToken: loginData.refresh_token, + user: loginData.user, + expiresAt: loginData.expires_at * 1000, // 转换为毫秒时间戳 + loginTime: Date.now() + }; + + // 保存到本地存储 + wx.setStorageSync('userInfo', userInfo); + + // 更新全局状态 + const app = this.getApp(); + if (app) { + app.globalData.userInfo = userInfo; + app.globalData.isLoggedIn = true; + } + + // 设置API客户端token + apiClient.setToken(userInfo.token); + + console.log('认证信息保存成功,用户customId:', userInfo.user?.customId); + + // 验证保存是否成功 + const savedInfo = wx.getStorageSync('userInfo'); + console.log('验证保存结果:', { + saved: !!savedInfo, + customId: savedInfo?.user?.customId, + hasToken: !!savedInfo?.token + }); + + return true; + + } catch (error) { + console.error('保存认证信息失败:', error); + return false; + } + } + + // 清除认证数据 + clearAuthData() { + try { + // 清除本地存储 + wx.removeStorageSync('userInfo'); + + // 清除全局状态 + const app = this.getApp(); + if (app) { + app.globalData.userInfo = null; + app.globalData.isLoggedIn = false; + } + + // 清除API客户端token + apiClient.clearToken(); + + console.log('认证数据已清除'); + + } catch (error) { + console.error('清除认证数据失败:', error); + } + } + + // 登出 + async logout() { + try { + console.log('开始登出'); + + // 调用登出API + try { + await apiClient.logout(); + console.log('服务端登出成功'); + } catch (error) { + console.error('服务端登出失败,继续本地清理:', error); + } + + // 清除本地认证数据 + this.clearAuthData(); + + console.log('登出完成'); + return true; + + } catch (error) { + console.error('登出失败:', error); + // 即使失败也要清除本地数据 + this.clearAuthData(); + return false; + } + } + + // 检查并跳转到登录页 + requireAuth() { + if (!this.isLoggedIn()) { + console.log('需要登录,跳转到登录页'); + wx.reLaunch({ + url: '/pages/login/login' + }); + return false; + } + return true; + } + + // 静默登录(小程序启动时调用) + async silentLogin() { + // 防止并发登录 + if (this.loginPromise) { + return await this.loginPromise; + } + + console.log('开始静默登录'); + + this.loginPromise = this._doSilentLogin(); + + try { + const result = await this.loginPromise; + return result; + } finally { + this.loginPromise = null; + } + } + + // 执行静默登录 + async _doSilentLogin() { + try { + // 先检查本地状态 + if (!this.isInitialized) { + await this.init(); + } + + // 如果已经登录且token有效 + const app = this.getApp(); + const userInfo = app?.globalData?.userInfo; + if (this.isLoggedIn() && this.isTokenValid(userInfo)) { + console.log('当前登录状态有效,无需静默登录'); + return true; + } + + // 尝试刷新token + const storedUserInfo = userInfo || wx.getStorageSync('userInfo'); + if (storedUserInfo?.refreshToken) { + console.log('尝试使用refresh token恢复登录'); + const refreshResult = await this.refreshTokenIfNeeded(storedUserInfo); + if (refreshResult) { + console.log('静默登录成功(通过token刷新)'); + return true; + } + } + + console.log('静默登录失败,需要用户手动登录'); + return false; + + } catch (error) { + console.error('静默登录异常:', error); + this.clearAuthData(); + return false; + } + } + + // 获取用户基本信息(用于页面显示) + getUserDisplayInfo() { + const user = this.getCurrentUser(); + if (!user) { + return null; + } + + return { + nickname: user.nickname || user.username || '未知用户', + avatar: user.avatar || '', + phone: user.phone || '', + customId: user.customId || user.custom_id || '' + }; + } + + // 检查特定权限 + hasPermission(permission) { + const user = this.getCurrentUser(); + if (!user) { + return false; + } + + // 这里可以根据实际需求检查用户权限 + // 例如:user.permissions?.includes(permission) + return true; + } + + // 等待认证初始化完成 + async waitForInitialization() { + if (this.isInitialized) { + return; + } + + return new Promise((resolve) => { + const checkInterval = setInterval(() => { + if (this.isInitialized) { + clearInterval(checkInterval); + resolve(); + } + }, 100); + }); + } + + // 🔥 调试方法:检查当前认证状态 + debugAuthStatus() { + try { + const app = this.getApp(); + const globalUserInfo = app?.globalData?.userInfo; + const storedUserInfo = wx.getStorageSync('userInfo'); + const apiToken = apiClient.getToken(); + + console.log('=== 认证状态调试信息 ==='); + console.log('1. 全局状态:', { + isLoggedIn: app?.globalData?.isLoggedIn, + hasGlobalUserInfo: !!globalUserInfo, + globalCustomId: globalUserInfo?.user?.customId, + globalToken: globalUserInfo?.token ? globalUserInfo.token.substring(0, 20) + '...' : null + }); + + console.log('2. 本地存储:', { + hasStoredUserInfo: !!storedUserInfo, + storedCustomId: storedUserInfo?.user?.customId, + storedToken: storedUserInfo?.token ? storedUserInfo.token.substring(0, 20) + '...' : null + }); + + console.log('3. API客户端:', { + hasApiToken: !!apiToken, + apiToken: apiToken ? apiToken.substring(0, 20) + '...' : null + }); + + console.log('4. 一致性检查:', { + globalVsStored: globalUserInfo?.user?.customId === storedUserInfo?.user?.customId, + globalVsApi: globalUserInfo?.token === apiToken, + storedVsApi: storedUserInfo?.token === apiToken + }); + console.log('========================'); + + } catch (error) { + console.error('调试认证状态失败:', error); + } + } +} + +// 创建全局实例 +const authManager = new AuthManager(); + +// 导出认证管理器 +module.exports = authManager; \ No newline at end of file diff --git a/utils/chat-api.js b/utils/chat-api.js new file mode 100644 index 0000000..3f73ad7 --- /dev/null +++ b/utils/chat-api.js @@ -0,0 +1,326 @@ +// 聊天功能API客户端 +const apiClient = require('./api-client.js'); + +class ChatAPI { + constructor() { + this.apiClient = apiClient; + } + + // 🔥 ===== 历史消息查询 ===== + + // 获取历史消息 + async getHistory(receiverId, chatType, options = {}) { + try { + const params = { + receiverId: receiverId, + chatType: chatType, + limit: options.limit || 20, + ...options + }; + + const response = await this.apiClient.get('/api/v1/chat/history', params); + return response; + } catch (error) { + console.error('获取历史消息失败:', error); + throw error; + } + } + + // 获取会话消息 + async getConversationMessages(conversationId, options = {}) { + try { + const params = { + limit: options.limit || 20, + ...options + }; + + const response = await this.apiClient.get(`/api/v1/chat/conversation/${conversationId}/messages`, params); + return response; + } catch (error) { + console.error('获取会话消息失败:', error); + throw error; + } + } + + // 🔥 ===== 会话管理 ===== + + // 获取会话列表 + async getConversations() { + try { + const response = await this.apiClient.get('/api/v1/chat/conversations'); + return response; + } catch (error) { + console.error('获取会话列表失败:', error); + throw error; + } + } + + // 更新会话设置 + async updateConversation(conversationId, settings) { + try { + const response = await this.apiClient.put(`/api/v1/chat/conversation/${conversationId}`, settings); + return response; + } catch (error) { + console.error('更新会话设置失败:', error); + throw error; + } + } + + // 删除会话 + async deleteConversation(conversationId) { + try { + const response = await this.apiClient.delete(`/api/v1/chat/conversation/${conversationId}`); + return response; + } catch (error) { + console.error('删除会话失败:', error); + throw error; + } + } + + // 🔥 ===== 消息已读管理 ===== + + // 标记单条消息已读 + async markMessageRead(messageId, conversationId) { + try { + const response = await this.apiClient.post('/api/v1/chat/read', { + messageId: messageId, + conversationId: conversationId + }); + return response; + } catch (error) { + console.error('标记消息已读失败:', error); + throw error; + } + } + + // 批量标记消息已读 + async batchMarkRead(messageIds, conversationId) { + try { + const response = await this.apiClient.post('/api/v1/chat/batch-read', { + messageIds: messageIds, + conversationId: conversationId + }); + return response; + } catch (error) { + console.error('批量标记已读失败:', error); + throw error; + } + } + + // 标记全部消息已读 + async markAllRead(conversationId) { + try { + const response = await this.apiClient.post('/api/v1/chat/mark-all-read', { + conversationId: conversationId + }); + return response; + } catch (error) { + console.error('标记全部已读失败:', error); + throw error; + } + } + + // 🔥 ===== 未读消息统计 ===== + + // 获取总未读数 + async getTotalUnreadCount() { + try { + const response = await this.apiClient.get('/api/v1/chat/unread/total'); + return response; + } catch (error) { + console.error('获取总未读数失败:', error); + throw error; + } + } + + // 🔥 ===== 表情包管理 ===== + + // 获取表情包列表 + async getEmojiPackages() { + try { + const response = await this.apiClient.get('/api/v1/chat/emoji/packages'); + return response; + } catch (error) { + console.error('获取表情包失败:', error); + throw error; + } + } + + // 🔥 ===== 聊天设置 ===== + + // 获取聊天设置 + async getChatSettings() { + try { + const response = await this.apiClient.get('/api/v1/chat/settings'); + return response; + } catch (error) { + console.error('获取聊天设置失败:', error); + throw error; + } + } + + // 更新聊天设置 + async updateChatSettings(settings) { + try { + const response = await this.apiClient.put('/api/v1/chat/settings', settings); + return response; + } catch (error) { + console.error('更新聊天设置失败:', error); + throw error; + } + } + + // 🔥 ===== 聊天记录备份 ===== + + // 备份聊天记录 + async backupChatHistory(options = {}) { + try { + const response = await this.apiClient.post('/api/v1/chat/backup', options); + return response; + } catch (error) { + console.error('备份聊天记录失败:', error); + throw error; + } + } + + // 恢复聊天记录 + async restoreChatHistory(backupId) { + try { + const response = await this.apiClient.post('/api/v1/chat/restore', { + backupId: backupId + }); + return response; + } catch (error) { + console.error('恢复聊天记录失败:', error); + throw error; + } + } + + // 获取备份列表 + async getBackupList() { + try { + const response = await this.apiClient.get('/api/v1/chat/backups'); + return response; + } catch (error) { + console.error('获取备份列表失败:', error); + throw error; + } + } + + // 🔥 ===== 虚拟弹幕 ===== + + // 发送弹幕 + async sendDanmaku(content, latitude, longitude, expiresIn = 3600) { + try { + const response = await this.apiClient.post('/api/v1/chat/danmaku', { + content: content, + latitude: latitude, + longitude: longitude, + expiresIn: expiresIn + }); + return response; + } catch (error) { + console.error('发送弹幕失败:', error); + throw error; + } + } + + // 获取附近弹幕 + async getNearbyDanmaku(latitude, longitude, radius = 1000) { + try { + const params = { + latitude: latitude, + longitude: longitude, + radius: radius + }; + const response = await this.apiClient.get('/api/v1/chat/danmaku/nearby', params); + return response; + } catch (error) { + console.error('获取附近弹幕失败:', error); + throw error; + } + } + + // 🔥 ===== 离线消息同步 ===== + + // 拉取离线消息 + async pullOfflineMessages(lastSeqId, deviceId, limit = 100) { + try { + const params = { + lastSeqId: lastSeqId, + deviceId: deviceId, + limit: limit + }; + const response = await this.apiClient.get('/api/v1/chat/sync/pull', params); + return response; + } catch (error) { + console.error('拉取离线消息失败:', error); + throw error; + } + } + + // 确认消息状态 + async ackMessages(messageIds, deviceId) { + try { + const response = await this.apiClient.post('/api/v1/chat/sync/ack', { + messageIds: messageIds, + deviceId: deviceId + }); + return response; + } catch (error) { + console.error('确认消息状态失败:', error); + throw error; + } + } + + // 获取同步状态 + async getSyncStatus() { + try { + const response = await this.apiClient.get('/api/v1/chat/sync/status'); + return response; + } catch (error) { + console.error('获取同步状态失败:', error); + throw error; + } + } + + // 🔥 ===== 工具方法 ===== + + // 生成会话ID + generateConversationId(userId1, userId2, chatType) { + if (chatType === 0) { + // 单聊:确保较小的ID在前 + const ids = [userId1, userId2].sort(); + return `s_${ids[0]}_${ids[1]}`; + } else { + // 群聊 + return `g_${userId2}`; + } + } + + // 解析会话ID + parseConversationId(conversationId) { + if (conversationId.startsWith('s_')) { + // 单聊 + const parts = conversationId.split('_'); + return { + type: 0, + userId1: parts[1], + userId2: parts[2] + }; + } else if (conversationId.startsWith('g_')) { + // 群聊 + const groupId = conversationId.substring(2); + return { + type: 1, + groupId: groupId + }; + } + return null; + } +} + +// 创建全局单例 +const chatAPI = new ChatAPI(); + +module.exports = chatAPI; diff --git a/utils/chat-message-handler.js b/utils/chat-message-handler.js new file mode 100644 index 0000000..a20ecef --- /dev/null +++ b/utils/chat-message-handler.js @@ -0,0 +1,300 @@ +/** + * 聊天消息处理器 + * 专门处理WebSocket接收到的聊天相关消息 + */ +class ChatMessageHandler { + constructor() { + this.messageListeners = new Map(); + this.statusListeners = new Map(); + this.conversationListeners = new Map(); + } + + // 处理WebSocket消息 + handleMessage(message) { + console.log('🔄 聊天消息处理器收到消息:', message.type); + + switch (message.type) { + case 'new_message': + this.handleNewMessage(message.data); + break; + case 'message_status_update': + this.handleMessageStatusUpdate(message.data); + break; + case 'message_recalled': + this.handleMessageRecalled(message.data); + break; + case 'conversation_update': + this.handleConversationUpdate(message.data); + break; + case 'typing_status': + this.handleTypingStatus(message.data); + break; + case 'read_receipt': + this.handleReadReceipt(message.data); + break; + default: + console.log('未处理的聊天消息类型:', message.type); + } + } + + // 处理新消息 + handleNewMessage(messageData) { + console.log('📨 收到新消息:', messageData); + + // 格式化消息数据 + const formattedMessage = this.formatMessage(messageData); + + // 通知所有监听器 + this.notifyMessageListeners('new_message', formattedMessage); + + // 更新会话信息 + this.updateConversationLastMessage(formattedMessage); + } + + // 处理消息状态更新 + handleMessageStatusUpdate(statusData) { + console.log('📊 消息状态更新:', statusData); + + this.notifyStatusListeners('status_update', statusData); + } + + // 处理消息撤回 + handleMessageRecalled(recallData) { + console.log('↩️ 消息被撤回:', recallData); + + this.notifyMessageListeners('message_recalled', recallData); + } + + // 处理会话更新 + handleConversationUpdate(conversationData) { + console.log('💬 会话更新:', conversationData); + + this.notifyConversationListeners('conversation_update', conversationData); + } + + // 处理输入状态 + handleTypingStatus(typingData) { + console.log('⌨️ 输入状态:', typingData); + + this.notifyMessageListeners('typing_status', typingData); + } + + // 处理已读回执 + handleReadReceipt(readData) { + console.log('✅ 已读回执:', readData); + + this.notifyStatusListeners('read_receipt', readData); + } + + // 格式化消息数据 + formatMessage(messageData) { + return { + messageId: messageData.messageId, + senderId: messageData.senderId, + receiverId: messageData.receiverId, + conversationId: messageData.conversationId || this.generateConversationId(messageData.senderId, messageData.receiverId), + chatType: messageData.chatType || 0, + msgType: messageData.msgType || 1, + content: messageData.content || '', + status: messageData.status || 1, + sendTime: messageData.sendTime || new Date().toISOString(), + senderName: messageData.senderName || '', + senderAvatar: messageData.senderAvatar || '', + replyTo: messageData.replyTo || '', + atUsers: messageData.atUsers || [], + extra: messageData.extra || '', + // 客户端扩展字段 + isOwn: false, // 需要在使用时设置 + displayTime: this.formatDisplayTime(messageData.sendTime), + contentType: this.getContentType(messageData.msgType), + parsedContent: this.parseContent(messageData.content, messageData.msgType) + }; + } + + // 生成会话ID + generateConversationId(senderId, receiverId) { + // 单聊会话ID生成规则:较小的ID在前 + const ids = [senderId, receiverId].sort(); + return `conv_${ids[0]}_${ids[1]}`; + } + + // 格式化显示时间 + formatDisplayTime(timeString) { + if (!timeString) return ''; + + const messageTime = new Date(timeString); + const now = new Date(); + const diffMs = now - messageTime; + const diffMinutes = Math.floor(diffMs / (1000 * 60)); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffMinutes < 1) { + return '刚刚'; + } else if (diffMinutes < 60) { + return `${diffMinutes}分钟前`; + } else if (diffHours < 24) { + return messageTime.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); + } else if (diffDays < 7) { + const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; + return weekdays[messageTime.getDay()]; + } else { + return messageTime.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }); + } + } + + // 获取内容类型 + getContentType(msgType) { + const typeMap = { + 1: 'text', + 2: 'image', + 3: 'voice', + 4: 'video', + 5: 'file', + 6: 'emoji', + 7: 'location', + 8: 'system', + 9: 'recall', + 10: 'redpacket' + }; + return typeMap[msgType] || 'text'; + } + + // 解析消息内容 + parseContent(content, msgType) { + try { + switch (msgType) { + case 2: // 图片 + return { url: content, type: 'image' }; + case 3: // 语音 + const voiceData = JSON.parse(content); + return { url: voiceData.url, duration: voiceData.duration, type: 'voice' }; + case 4: // 视频 + const videoData = JSON.parse(content); + return { url: videoData.url, duration: videoData.duration, thumbnail: videoData.thumbnail, type: 'video' }; + case 5: // 文件 + const fileData = JSON.parse(content); + return { url: fileData.url, name: fileData.name, size: fileData.size, type: 'file' }; + case 7: // 位置 + const locationData = JSON.parse(content); + return { latitude: locationData.latitude, longitude: locationData.longitude, address: locationData.address, type: 'location' }; + default: + return { text: content, type: 'text' }; + } + } catch (error) { + console.warn('解析消息内容失败:', error); + return { text: content, type: 'text' }; + } + } + + // 更新会话最后消息 + updateConversationLastMessage(message) { + const conversationUpdate = { + conversationId: message.conversationId, + lastMessage: { + content: this.getDisplayContent(message), + sendTime: message.sendTime, + senderId: message.senderId, + senderName: message.senderName + }, + unreadCount: message.isOwn ? 0 : 1 // 如果是自己发的消息,未读数为0 + }; + + this.notifyConversationListeners('last_message_update', conversationUpdate); + } + + // 获取显示内容 + getDisplayContent(message) { + switch (message.msgType) { + case 1: return message.content; + case 2: return '[图片]'; + case 3: return '[语音]'; + case 4: return '[视频]'; + case 5: return '[文件]'; + case 6: return '[表情]'; + case 7: return '[位置]'; + case 8: return message.content; + case 9: return '撤回了一条消息'; + case 10: return '[红包]'; + default: return message.content; + } + } + + // 注册消息监听器 + onMessage(event, listener) { + if (!this.messageListeners.has(event)) { + this.messageListeners.set(event, []); + } + this.messageListeners.get(event).push(listener); + } + + // 注册状态监听器 + onStatus(event, listener) { + if (!this.statusListeners.has(event)) { + this.statusListeners.set(event, []); + } + this.statusListeners.get(event).push(listener); + } + + // 注册会话监听器 + onConversation(event, listener) { + if (!this.conversationListeners.has(event)) { + this.conversationListeners.set(event, []); + } + this.conversationListeners.get(event).push(listener); + } + + // 通知消息监听器 + notifyMessageListeners(event, data) { + const listeners = this.messageListeners.get(event); + if (listeners) { + listeners.forEach(listener => { + try { + listener(data); + } catch (error) { + console.error(`消息监听器错误 [${event}]:`, error); + } + }); + } + } + + // 通知状态监听器 + notifyStatusListeners(event, data) { + const listeners = this.statusListeners.get(event); + if (listeners) { + listeners.forEach(listener => { + try { + listener(data); + } catch (error) { + console.error(`状态监听器错误 [${event}]:`, error); + } + }); + } + } + + // 通知会话监听器 + notifyConversationListeners(event, data) { + const listeners = this.conversationListeners.get(event); + if (listeners) { + listeners.forEach(listener => { + try { + listener(data); + } catch (error) { + console.error(`会话监听器错误 [${event}]:`, error); + } + }); + } + } + + // 清理监听器 + removeAllListeners() { + this.messageListeners.clear(); + this.statusListeners.clear(); + this.conversationListeners.clear(); + } +} + +// 创建全局实例 +const chatMessageHandler = new ChatMessageHandler(); + +module.exports = chatMessageHandler; diff --git a/utils/device.js b/utils/device.js new file mode 100644 index 0000000..fedfa4f --- /dev/null +++ b/utils/device.js @@ -0,0 +1,475 @@ +// 设备工具类 - 对应Flutter的device_util.dart +const config = require('../config/config.js'); + +class DeviceUtil { + constructor() { + this.systemInfo = null; + this.deviceId = null; + this.initialized = false; + } + + // 初始化 + async init() { + try { + // 获取系统信息 + await this.getSystemInfo(); + + // 生成设备ID + this.generateDeviceId(); + + this.initialized = true; + console.log('设备工具初始化完成'); + console.log('设备信息:', this.getDeviceInfo()); + } catch (error) { + console.error('设备工具初始化失败:', error); + } + } + + // 获取系统信息 + async getSystemInfo() { + return new Promise((resolve, reject) => { + wx.getSystemInfo({ + success: (res) => { + this.systemInfo = res; + console.log('系统信息获取成功:', res); + resolve(res); + }, + fail: (error) => { + console.error('获取系统信息失败:', error); + reject(error); + } + }); + }); + } + + // 生成设备ID + generateDeviceId() { + // 小程序无法获取真正的设备ID,使用时间戳+随机数生成 + const timestamp = Date.now(); + const random = Math.floor(Math.random() * 1000000); + this.deviceId = `mp_${timestamp}_${random}`; + + // 尝试从存储中获取已保存的设备ID + try { + const savedDeviceId = wx.getStorageSync('lanmei_deviceId'); + if (savedDeviceId) { + this.deviceId = savedDeviceId; + } else { + // 保存生成的设备ID + wx.setStorageSync('lanmei_deviceId', this.deviceId); + } + } catch (error) { + console.error('设备ID操作失败:', error); + } + } + + // 获取设备ID + getDeviceId() { + if (!this.deviceId) { + this.generateDeviceId(); + } + return this.deviceId; + } + + // 获取设备型号 + getDeviceModel() { + if (!this.systemInfo) { + return 'Unknown'; + } + return this.systemInfo.model || 'Unknown'; + } + + // 获取设备类型 - 与Flutter项目保持一致 + getDeviceType() { + // 小程序使用android类型,后端只接受'android'或'ios' + return 'android'; + } + + // 获取系统版本 + getSystemVersion() { + if (!this.systemInfo) { + return 'Unknown'; + } + return this.systemInfo.system || 'Unknown'; + } + + // 获取微信版本 + getWeChatVersion() { + if (!this.systemInfo) { + return 'Unknown'; + } + return this.systemInfo.version || 'Unknown'; + } + + // 获取应用版本 + getAppVersion() { + return config.appVersion; + } + + // 获取平台信息 + getPlatform() { + if (!this.systemInfo) { + return 'unknown'; + } + return this.systemInfo.platform || 'unknown'; + } + + // 获取屏幕信息 + getScreenInfo() { + if (!this.systemInfo) { + return { + screenWidth: 375, + screenHeight: 812, + windowWidth: 375, + windowHeight: 812, + pixelRatio: 2 + }; + } + + return { + screenWidth: this.systemInfo.screenWidth, + screenHeight: this.systemInfo.screenHeight, + windowWidth: this.systemInfo.windowWidth, + windowHeight: this.systemInfo.windowHeight, + pixelRatio: this.systemInfo.pixelRatio, + statusBarHeight: this.systemInfo.statusBarHeight + }; + } + + // 获取网络类型 + async getNetworkType() { + return new Promise((resolve) => { + wx.getNetworkType({ + success: (res) => { + console.log('网络类型:', res.networkType); + resolve(res.networkType); + }, + fail: (error) => { + console.error('获取网络类型失败:', error); + resolve('unknown'); + } + }); + }); + } + + // 获取电量信息 + async getBatteryInfo() { + return new Promise((resolve) => { + wx.getBatteryInfo({ + success: (res) => { + console.log('电量信息:', res); + resolve({ + level: res.level, + isCharging: res.isCharging + }); + }, + fail: (error) => { + console.error('获取电量信息失败:', error); + resolve({ + level: 100, + isCharging: false + }); + } + }); + }); + } + + // 获取指南针信息 + async getCompassInfo() { + return new Promise((resolve) => { + // 先启动指南针 + wx.startCompass({ + success: () => { + // 监听指南针数据 + wx.onCompassChange((res) => { + resolve({ + direction: res.direction, + accuracy: res.accuracy + }); + // 取消监听 + wx.offCompassChange(); + }); + }, + fail: (error) => { + console.error('启动指南针失败:', error); + resolve({ + direction: 0, + accuracy: 0 + }); + } + }); + }); + } + + // 获取加速计信息 + async getAccelerometerInfo() { + return new Promise((resolve) => { + wx.startAccelerometer({ + interval: 'normal', + success: () => { + wx.onAccelerometerChange((res) => { + resolve({ + x: res.x, + y: res.y, + z: res.z + }); + // 取消监听 + wx.offAccelerometerChange(); + wx.stopAccelerometer(); + }); + }, + fail: (error) => { + console.error('启动加速计失败:', error); + resolve({ + x: 0, + y: 0, + z: 0 + }); + } + }); + }); + } + + // 获取陀螺仪信息 + async getGyroscopeInfo() { + return new Promise((resolve) => { + wx.startGyroscope({ + interval: 'normal', + success: () => { + wx.onGyroscopeChange((res) => { + resolve({ + x: res.x, + y: res.y, + z: res.z + }); + // 取消监听 + wx.offGyroscopeChange(); + wx.stopGyroscope(); + }); + }, + fail: (error) => { + console.error('启动陀螺仪失败:', error); + resolve({ + x: 0, + y: 0, + z: 0 + }); + } + }); + }); + } + + // 获取设备方向 + async getDeviceOrientation() { + return new Promise((resolve) => { + wx.startDeviceMotionListening({ + interval: 'normal', + success: () => { + wx.onDeviceMotionChange((res) => { + resolve({ + alpha: res.alpha, + beta: res.beta, + gamma: res.gamma + }); + // 取消监听 + wx.offDeviceMotionChange(); + wx.stopDeviceMotionListening(); + }); + }, + fail: (error) => { + console.error('启动设备方向监听失败:', error); + resolve({ + alpha: 0, + beta: 0, + gamma: 0 + }); + } + }); + }); + } + + // 获取完整设备信息 + getDeviceInfo() { + return { + deviceId: this.getDeviceId(), + model: this.getDeviceModel(), + type: this.getDeviceType(), + system: this.getSystemVersion(), + platform: this.getPlatform(), + wechatVersion: this.getWeChatVersion(), + appVersion: this.getAppVersion(), + screen: this.getScreenInfo() + }; + } + + // 监听电量变化 + onBatteryLevelChange(callback) { + if (this.canUseApi('onBatteryLevelChange')) { + wx.onBatteryLevelChange(callback); + } else { + console.warn('当前环境不支持电量变化监听API'); + // 提供模拟回调 + setTimeout(() => { + callback({ + level: 100, + isCharging: false + }); + }, 100); + } + } + + // 监听指南针变化 + onCompassChange(callback) { + wx.startCompass({ + success: () => { + wx.onCompassChange(callback); + }, + fail: (error) => { + console.error('启动指南针监听失败:', error); + } + }); + } + + // 监听加速计变化 + onAccelerometerChange(callback, interval = 'normal') { + wx.startAccelerometer({ + interval: interval, + success: () => { + wx.onAccelerometerChange(callback); + }, + fail: (error) => { + console.error('启动加速计监听失败:', error); + } + }); + } + + // 监听陀螺仪变化 + onGyroscopeChange(callback, interval = 'normal') { + wx.startGyroscope({ + interval: interval, + success: () => { + wx.onGyroscopeChange(callback); + }, + fail: (error) => { + console.error('启动陀螺仪监听失败:', error); + } + }); + } + + // 监听设备方向变化 + onDeviceMotionChange(callback, interval = 'normal') { + wx.startDeviceMotionListening({ + interval: interval, + success: () => { + wx.onDeviceMotionChange(callback); + }, + fail: (error) => { + console.error('启动设备方向监听失败:', error); + } + }); + } + + // 停止所有传感器监听 + stopAllSensors() { + try { + // 安全地停止传感器 + if (this.canUseApi('stopCompass')) wx.stopCompass(); + if (this.canUseApi('stopAccelerometer')) wx.stopAccelerometer(); + if (this.canUseApi('stopGyroscope')) wx.stopGyroscope(); + if (this.canUseApi('stopDeviceMotionListening')) wx.stopDeviceMotionListening(); + + // 安全地取消监听 + if (this.canUseApi('offBatteryLevelChange')) wx.offBatteryLevelChange(); + if (this.canUseApi('offCompassChange')) wx.offCompassChange(); + if (this.canUseApi('offAccelerometerChange')) wx.offAccelerometerChange(); + if (this.canUseApi('offGyroscopeChange')) wx.offGyroscopeChange(); + if (this.canUseApi('offDeviceMotionChange')) wx.offDeviceMotionChange(); + + console.log('所有传感器监听已停止'); + } catch (error) { + console.error('停止传感器监听失败:', error); + } + } + + // 检查是否支持指定API + canUseApi(apiName) { + return typeof wx[apiName] === 'function'; + } + + // 获取支持的API列表 + getSupportedApis() { + const apis = [ + 'getBatteryInfo', + 'startCompass', + 'startAccelerometer', + 'startGyroscope', + 'startDeviceMotionListening', + 'getLocation', + 'chooseLocation', + 'openLocation' + ]; + + const supportedApis = []; + apis.forEach(api => { + if (this.canUseApi(api)) { + supportedApis.push(api); + } + }); + + return supportedApis; + } + + // 振动反馈 + vibrate(type = 'short') { + if (type === 'short') { + wx.vibrateShort({ + success: () => console.log('短震动成功'), + fail: (error) => console.error('短震动失败:', error) + }); + } else if (type === 'long') { + wx.vibrateLong({ + success: () => console.log('长震动成功'), + fail: (error) => console.error('长震动失败:', error) + }); + } + } + + // 设置屏幕亮度 + setScreenBrightness(value) { + wx.setScreenBrightness({ + value: value, + success: () => console.log('设置屏幕亮度成功:', value), + fail: (error) => console.error('设置屏幕亮度失败:', error) + }); + } + + // 获取屏幕亮度 + async getScreenBrightness() { + return new Promise((resolve) => { + wx.getScreenBrightness({ + success: (res) => { + console.log('屏幕亮度:', res.value); + resolve(res.value); + }, + fail: (error) => { + console.error('获取屏幕亮度失败:', error); + resolve(0.5); + } + }); + }); + } + + // 保持屏幕常亮 + keepScreenOn(keepOn = true) { + wx.setKeepScreenOn({ + keepScreenOn: keepOn, + success: () => console.log('设置屏幕常亮:', keepOn), + fail: (error) => console.error('设置屏幕常亮失败:', error) + }); + } +} + +// 创建单例 +const deviceUtil = new DeviceUtil(); + +module.exports = deviceUtil; \ No newline at end of file diff --git a/utils/error-handler.js b/utils/error-handler.js new file mode 100644 index 0000000..07719c1 --- /dev/null +++ b/utils/error-handler.js @@ -0,0 +1,692 @@ +// 全局错误处理管理器 - 微信小程序专用 +// 统一处理应用中的各种错误,提供错误恢复和降级方案 + +const performanceMonitor = require('./performance-monitor.js'); + +/** + * 全局错误处理管理器 + * 功能: + * 1. 全局错误捕获和处理 + * 2. 错误分类和分析 + * 3. 自动重试机制 + * 4. 降级方案 + * 5. 错误恢复策略 + * 6. 用户友好的错误提示 + */ +class ErrorHandler { + constructor() { + this.isInitialized = false; + + // 错误处理配置 + this.config = { + // 错误处理开关 + enabled: true, + + // 自动重试配置 + retry: { + enabled: true, + maxAttempts: 3, + baseDelay: 1000, // 基础延迟 (ms) + maxDelay: 10000, // 最大延迟 (ms) + backoffFactor: 2 // 退避因子 + }, + + // 降级配置 + fallback: { + enabled: true, + cacheTimeout: 300000, // 缓存超时 (5分钟) + offlineMode: true // 离线模式 + }, + + // 用户提示配置 + userNotification: { + enabled: true, + showDetails: false, // 是否显示错误详情 + autoHide: true, // 自动隐藏 + hideDelay: 3000 // 隐藏延迟 (ms) + } + }; + + // 错误类型定义 + this.errorTypes = { + NETWORK_ERROR: 'network_error', + API_ERROR: 'api_error', + PARSE_ERROR: 'parse_error', + STORAGE_ERROR: 'storage_error', + PERMISSION_ERROR: 'permission_error', + VALIDATION_ERROR: 'validation_error', + UNKNOWN_ERROR: 'unknown_error' + }; + + // 错误统计 + this.errorStats = { + totalErrors: 0, + errorsByType: new Map(), + errorsByPage: new Map(), + recentErrors: [] + }; + + // 重试队列 + this.retryQueue = new Map(); + + // 降级缓存 + this.fallbackCache = new Map(); + + this.init(); + } + + // 初始化错误处理器 + init() { + if (this.isInitialized || !this.config.enabled) return; + + console.log('🚨 初始化全局错误处理器...'); + + try { + // 设置全局错误监听 + this.setupGlobalErrorHandlers(); + + // 设置网络错误监听 + this.setupNetworkErrorHandlers(); + + // 设置Promise错误监听 + this.setupPromiseErrorHandlers(); + + this.isInitialized = true; + console.log('✅ 全局错误处理器初始化完成'); + + } catch (error) { + console.error('❌ 全局错误处理器初始化失败:', error); + } + } + + // 🚨 ===== 错误捕获和处理 ===== + + // 处理错误 + handleError(error, context = {}) { + if (!this.config.enabled) return; + + try { + // 错误分类 + const errorType = this.classifyError(error); + + // 创建错误信息 + const errorInfo = this.createErrorInfo(error, errorType, context); + + // 更新错误统计 + this.updateErrorStats(errorInfo); + + // 记录错误到性能监控 + performanceMonitor.recordError(error, context); + + // 处理特定类型的错误 + this.handleSpecificError(errorInfo); + + // 显示用户提示 + this.showUserNotification(errorInfo); + + console.error('🚨 错误处理:', errorInfo); + + return errorInfo; + + } catch (handlerError) { + console.error('❌ 错误处理器本身出错:', handlerError); + } + } + + // 错误分类 + classifyError(error) { + if (!error) return this.errorTypes.UNKNOWN_ERROR; + + const message = error.message || error.toString(); + const stack = error.stack || ''; + + // 网络错误 + if (message.includes('network') || message.includes('timeout') || + message.includes('连接') || error.code === 'NETWORK_ERROR') { + return this.errorTypes.NETWORK_ERROR; + } + + // API错误 + if (message.includes('API') || message.includes('request') || + message.includes('response') || error.statusCode) { + return this.errorTypes.API_ERROR; + } + + // 解析错误 + if (message.includes('JSON') || message.includes('parse') || + message.includes('Unexpected token')) { + return this.errorTypes.PARSE_ERROR; + } + + // 存储错误 + if (message.includes('storage') || message.includes('setStorage') || + message.includes('getStorage')) { + return this.errorTypes.STORAGE_ERROR; + } + + // 权限错误 + if (message.includes('permission') || message.includes('unauthorized') || + message.includes('权限') || error.code === 'PERMISSION_DENIED') { + return this.errorTypes.PERMISSION_ERROR; + } + + // 验证错误 + if (message.includes('validation') || message.includes('invalid') || + message.includes('验证')) { + return this.errorTypes.VALIDATION_ERROR; + } + + return this.errorTypes.UNKNOWN_ERROR; + } + + // 创建错误信息 + createErrorInfo(error, errorType, context) { + return { + id: this.generateErrorId(), + timestamp: Date.now(), + type: errorType, + message: error.message || error.toString(), + stack: error.stack || null, + code: error.code || null, + statusCode: error.statusCode || null, + context: context, + pagePath: this.getCurrentPagePath(), + userAgent: this.getUserAgent(), + canRetry: this.canRetry(errorType), + canFallback: this.canFallback(errorType), + severity: this.getErrorSeverity(errorType) + }; + } + + // 🔄 ===== 自动重试机制 ===== + + // 自动重试 + async autoRetry(operation, context = {}) { + if (!this.config.retry.enabled) { + return await operation(); + } + + const retryId = this.generateRetryId(); + let lastError = null; + + for (let attempt = 1; attempt <= this.config.retry.maxAttempts; attempt++) { + try { + // 记录重试尝试 + if (attempt > 1) { + console.log(`🔄 重试第 ${attempt - 1} 次:`, context); + } + + const result = await operation(); + + // 成功,清除重试记录 + this.retryQueue.delete(retryId); + + return result; + + } catch (error) { + lastError = error; + + // 检查是否可以重试 + if (!this.canRetry(this.classifyError(error)) || attempt >= this.config.retry.maxAttempts) { + break; + } + + // 计算延迟时间 + const delay = this.calculateRetryDelay(attempt); + + // 记录重试信息 + this.retryQueue.set(retryId, { + attempt: attempt, + nextRetry: Date.now() + delay, + context: context, + error: error + }); + + // 等待重试 + await this.sleep(delay); + } + } + + // 所有重试都失败了 + this.retryQueue.delete(retryId); + throw lastError; + } + + // 计算重试延迟 + calculateRetryDelay(attempt) { + const delay = this.config.retry.baseDelay * Math.pow(this.config.retry.backoffFactor, attempt - 1); + return Math.min(delay, this.config.retry.maxDelay); + } + + // 检查是否可以重试 + canRetry(errorType) { + const retryableErrors = [ + this.errorTypes.NETWORK_ERROR, + this.errorTypes.API_ERROR + ]; + + return retryableErrors.includes(errorType); + } + + // 🔄 ===== 降级方案 ===== + + // 降级处理 + async fallbackHandler(operation, fallbackKey, fallbackData = null) { + if (!this.config.fallback.enabled) { + return await operation(); + } + + try { + const result = await operation(); + + // 成功时更新缓存 + this.updateFallbackCache(fallbackKey, result); + + return result; + + } catch (error) { + console.warn('🔄 操作失败,尝试降级方案:', error.message); + + // 尝试从缓存获取数据 + const cachedData = this.getFallbackCache(fallbackKey); + if (cachedData) { + console.log('✅ 使用缓存数据作为降级方案'); + return cachedData; + } + + // 使用提供的降级数据 + if (fallbackData !== null) { + console.log('✅ 使用默认数据作为降级方案'); + return fallbackData; + } + + // 没有降级方案,重新抛出错误 + throw error; + } + } + + // 更新降级缓存 + updateFallbackCache(key, data) { + this.fallbackCache.set(key, { + data: data, + timestamp: Date.now() + }); + } + + // 获取降级缓存 + getFallbackCache(key) { + const cached = this.fallbackCache.get(key); + + if (!cached) return null; + + // 检查缓存是否过期 + if (Date.now() - cached.timestamp > this.config.fallback.cacheTimeout) { + this.fallbackCache.delete(key); + return null; + } + + return cached.data; + } + + // 检查是否可以降级 + canFallback(errorType) { + const fallbackableErrors = [ + this.errorTypes.NETWORK_ERROR, + this.errorTypes.API_ERROR + ]; + + return fallbackableErrors.includes(errorType); + } + + // 📱 ===== 用户提示 ===== + + // 显示用户提示 + showUserNotification(errorInfo) { + if (!this.config.userNotification.enabled) return; + + const userMessage = this.getUserFriendlyMessage(errorInfo); + + // 根据错误严重程度选择提示方式 + switch (errorInfo.severity) { + case 'low': + // 低严重程度,不显示提示或显示简单提示 + break; + + case 'medium': + wx.showToast({ + title: userMessage, + icon: 'none', + duration: this.config.userNotification.hideDelay + }); + break; + + case 'high': + wx.showModal({ + title: '操作失败', + content: userMessage, + showCancel: false, + confirmText: '确定' + }); + break; + + case 'critical': + wx.showModal({ + title: '严重错误', + content: userMessage + '\n\n建议重启应用或联系客服。', + showCancel: true, + cancelText: '稍后处理', + confirmText: '重启应用', + success: (res) => { + if (res.confirm) { + wx.reLaunch({ + url: '/pages/splash/splash' + }); + } + } + }); + break; + } + } + + // 获取用户友好的错误消息 + getUserFriendlyMessage(errorInfo) { + const messageMap = { + [this.errorTypes.NETWORK_ERROR]: '网络连接异常,请检查网络设置', + [this.errorTypes.API_ERROR]: '服务暂时不可用,请稍后重试', + [this.errorTypes.PARSE_ERROR]: '数据格式错误,请稍后重试', + [this.errorTypes.STORAGE_ERROR]: '存储空间不足或存储异常', + [this.errorTypes.PERMISSION_ERROR]: '权限不足,请检查相关权限设置', + [this.errorTypes.VALIDATION_ERROR]: '输入信息有误,请检查后重试', + [this.errorTypes.UNKNOWN_ERROR]: '操作失败,请稍后重试' + }; + + let message = messageMap[errorInfo.type] || messageMap[this.errorTypes.UNKNOWN_ERROR]; + + // 如果配置显示详情,添加错误详情 + if (this.config.userNotification.showDetails && errorInfo.message) { + message += `\n\n详情: ${errorInfo.message}`; + } + + return message; + } + + // 获取错误严重程度 + getErrorSeverity(errorType) { + const severityMap = { + [this.errorTypes.NETWORK_ERROR]: 'medium', + [this.errorTypes.API_ERROR]: 'medium', + [this.errorTypes.PARSE_ERROR]: 'high', + [this.errorTypes.STORAGE_ERROR]: 'high', + [this.errorTypes.PERMISSION_ERROR]: 'medium', + [this.errorTypes.VALIDATION_ERROR]: 'low', + [this.errorTypes.UNKNOWN_ERROR]: 'high' + }; + + return severityMap[errorType] || 'medium'; + } + + // 🔧 ===== 特定错误处理 ===== + + // 处理特定类型的错误 + handleSpecificError(errorInfo) { + switch (errorInfo.type) { + case this.errorTypes.NETWORK_ERROR: + this.handleNetworkError(errorInfo); + break; + + case this.errorTypes.API_ERROR: + this.handleApiError(errorInfo); + break; + + case this.errorTypes.STORAGE_ERROR: + this.handleStorageError(errorInfo); + break; + + case this.errorTypes.PERMISSION_ERROR: + this.handlePermissionError(errorInfo); + break; + + default: + this.handleGenericError(errorInfo); + } + } + + // 处理网络错误 + handleNetworkError(errorInfo) { + console.log('🌐 处理网络错误:', errorInfo.message); + + // 检查网络状态 + wx.getNetworkType({ + success: (res) => { + if (res.networkType === 'none') { + // 无网络连接 + this.handleOfflineMode(); + } + } + }); + } + + // 处理API错误 + handleApiError(errorInfo) { + console.log('🔌 处理API错误:', errorInfo.message); + + // 根据状态码进行特殊处理 + if (errorInfo.statusCode === 401) { + // 未授权,可能需要重新登录 + this.handleUnauthorizedError(); + } else if (errorInfo.statusCode >= 500) { + // 服务器错误,可以尝试重试 + console.log('🔄 服务器错误,建议重试'); + } + } + + // 处理存储错误 + handleStorageError(errorInfo) { + console.log('💾 处理存储错误:', errorInfo.message); + + // 尝试清理存储空间 + this.cleanupStorage(); + } + + // 处理权限错误 + handlePermissionError(errorInfo) { + console.log('🔐 处理权限错误:', errorInfo.message); + + // 可以引导用户去设置页面 + } + + // 处理通用错误 + handleGenericError(errorInfo) { + console.log('❓ 处理通用错误:', errorInfo.message); + } + + // 处理离线模式 + handleOfflineMode() { + if (!this.config.fallback.offlineMode) return; + + console.log('📱 进入离线模式'); + + // 可以设置全局离线状态 + // app.globalData.isOffline = true; + } + + // 处理未授权错误 + handleUnauthorizedError() { + console.log('🔐 处理未授权错误'); + + // 清除本地token + wx.removeStorageSync('token'); + + // 跳转到登录页面 + wx.reLaunch({ + url: '/pages/login/login' + }); + } + + // 清理存储空间 + cleanupStorage() { + try { + // 获取存储信息 + const storageInfo = wx.getStorageInfoSync(); + + if (storageInfo.currentSize > storageInfo.limitSize * 0.8) { + // 存储空间使用超过80%,进行清理 + console.log('🧹 开始清理存储空间'); + + // 清理缓存数据 + const keysToClean = ['cache_', 'temp_', 'old_']; + + storageInfo.keys.forEach(key => { + if (keysToClean.some(prefix => key.startsWith(prefix))) { + wx.removeStorageSync(key); + } + }); + + console.log('✅ 存储空间清理完成'); + } + + } catch (error) { + console.error('❌ 清理存储空间失败:', error); + } + } + + // 📊 ===== 错误统计 ===== + + // 更新错误统计 + updateErrorStats(errorInfo) { + this.errorStats.totalErrors++; + + // 按类型统计 + const typeCount = this.errorStats.errorsByType.get(errorInfo.type) || 0; + this.errorStats.errorsByType.set(errorInfo.type, typeCount + 1); + + // 按页面统计 + const pageCount = this.errorStats.errorsByPage.get(errorInfo.pagePath) || 0; + this.errorStats.errorsByPage.set(errorInfo.pagePath, pageCount + 1); + + // 记录最近错误 + this.errorStats.recentErrors.push(errorInfo); + + // 保持最近50条错误记录 + if (this.errorStats.recentErrors.length > 50) { + this.errorStats.recentErrors.shift(); + } + } + + // 获取错误统计 + getErrorStats() { + return { + totalErrors: this.errorStats.totalErrors, + errorsByType: Object.fromEntries(this.errorStats.errorsByType), + errorsByPage: Object.fromEntries(this.errorStats.errorsByPage), + recentErrors: this.errorStats.recentErrors.slice(-10), // 最近10条 + errorRate: this.calculateErrorRate() + }; + } + + // 计算错误率 + calculateErrorRate() { + const sessionDuration = Date.now() - (performanceMonitor.monitoringState?.startTime || Date.now()); + const hours = sessionDuration / (1000 * 60 * 60); + + return hours > 0 ? this.errorStats.totalErrors / hours : 0; + } + + // 🔧 ===== 工具方法 ===== + + // 设置全局错误监听 + setupGlobalErrorHandlers() { + // 微信小程序的错误监听 + wx.onError((error) => { + this.handleError(new Error(error), { source: 'global' }); + }); + + // 监听未处理的Promise拒绝 + wx.onUnhandledRejection((res) => { + this.handleError(res.reason, { source: 'unhandled_promise' }); + }); + } + + // 设置网络错误监听 + setupNetworkErrorHandlers() { + // 监听网络状态变化 + wx.onNetworkStatusChange((res) => { + if (!res.isConnected) { + this.handleError(new Error('Network disconnected'), { + source: 'network_change', + networkType: res.networkType + }); + } + }); + } + + // 设置Promise错误监听 + setupPromiseErrorHandlers() { + // 这个在微信小程序中通过wx.onUnhandledRejection已经处理 + } + + // 生成错误ID + generateErrorId() { + return `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + // 生成重试ID + generateRetryId() { + return `retry_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + // 获取当前页面路径 + getCurrentPagePath() { + try { + const pages = getCurrentPages(); + const currentPage = pages[pages.length - 1]; + return currentPage ? currentPage.route : 'unknown'; + } catch (error) { + return 'unknown'; + } + } + + // 获取用户代理 + getUserAgent() { + try { + // 使用新的API替代已弃用的wx.getSystemInfoSync + const deviceInfo = wx.getDeviceInfo(); + const appBaseInfo = wx.getAppBaseInfo(); + return `${deviceInfo.platform} ${deviceInfo.system} WeChat/${appBaseInfo.version}`; + } catch (error) { + // 兜底方案 + try { + const systemInfo = wx.getSystemInfoSync(); + return `${systemInfo.platform} ${systemInfo.system} WeChat/${systemInfo.version}`; + } catch (fallbackError) { + return 'unknown'; + } + } + } + + // 睡眠函数 + sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + // 销毁错误处理器 + destroy() { + this.errorStats = { + totalErrors: 0, + errorsByType: new Map(), + errorsByPage: new Map(), + recentErrors: [] + }; + + this.retryQueue.clear(); + this.fallbackCache.clear(); + + this.isInitialized = false; + console.log('🚨 全局错误处理器已销毁'); + } +} + +// 创建全局实例 +const errorHandler = new ErrorHandler(); + +module.exports = errorHandler; diff --git a/utils/friend-api.js b/utils/friend-api.js new file mode 100644 index 0000000..4355c7b --- /dev/null +++ b/utils/friend-api.js @@ -0,0 +1,324 @@ +// 好友功能API客户端 +const apiClient = require('./api-client.js'); + +class FriendAPI { + constructor() { + this.apiClient = apiClient; + } + + // 🔥 ===== 好友管理 ===== + + // 添加好友 + async addFriend(targetId, message = '') { + try { + const response = await this.apiClient.post('/api/v1/social/friend/add', { + targetId: targetId, + message: message + }); + return response; + } catch (error) { + console.error('添加好友失败:', error); + throw error; + } + } + + // 获取好友列表 + async getFriendList() { + try { + const response = await this.apiClient.get('/api/v1/social/friends'); + return response; + } catch (error) { + console.error('获取好友列表失败:', error); + throw error; + } + } + + // 获取好友详情 + async getFriendDetail(customId, lat = null, lng = null) { + try { + const params = {}; + if (lat !== null && lng !== null) { + params.lat = lat; + params.lng = lng; + } + + const response = await this.apiClient.get(`/api/v1/social/friends/${customId}/detail`, params); + return response; + } catch (error) { + console.error('获取好友详情失败:', error); + throw error; + } + } + + // 删除好友 + async deleteFriend(customId) { + try { + const response = await this.apiClient.delete(`/api/v1/social/friend/${customId}`); + return response; + } catch (error) { + console.error('删除好友失败:', error); + throw error; + } + } + + // 更新好友关系 + async updateFriendRelation(friendId, updates) { + try { + const response = await this.apiClient.put('/api/v1/social/friend', { + friendId: friendId, + ...updates + }); + return response; + } catch (error) { + console.error('更新好友关系失败:', error); + throw error; + } + } + + // 删除好友 + async deleteFriend(friendId) { + try { + const response = await this.apiClient.delete(`/api/v1/social/friend/${friendId}`); + return response; + } catch (error) { + console.error('删除好友失败:', error); + throw error; + } + } + + // 🔥 ===== 好友请求管理 ===== + + // 获取好友请求列表 + async getFriendRequests() { + try { + const response = await this.apiClient.get('/api/v1/social/friend/requests'); + return response; + } catch (error) { + console.error('获取好友请求列表失败:', error); + throw error; + } + } + + // 获取待处理好友请求数量 + async getFriendRequestCount() { + try { + const response = await this.apiClient.get('/api/v1/social/friend/requests/count'); + return response; + } catch (error) { + console.error('获取好友请求数量失败:', error); + throw error; + } + } + + // 处理好友请求 + async handleFriendRequest(requestId, accept) { + try { + const response = await this.apiClient.post('/api/v1/social/friend/handle-request', { + requestId: requestId, + accept: accept + }); + return response; + } catch (error) { + console.error('处理好友请求失败:', error); + throw error; + } + } + + // 批量处理好友请求 + async batchHandleFriendRequests(requestIds, accept) { + try { + const response = await this.apiClient.post('/api/v1/social/friend/batch-handle-requests', { + requestIds: requestIds, + accept: accept + }); + return response; + } catch (error) { + console.error('批量处理好友请求失败:', error); + throw error; + } + } + + // 🔥 ===== 用户搜索 ===== + + // 搜索用户 - 根据正确的接口文档 + async searchUsers(query, searchType = 'all', page = 1, pageSize = 10) { + try { + const response = await this.apiClient.post('/api/v1/social/users/search', { + query: query, + searchType: searchType, + page: page, + pageSize: pageSize + }); + return response; + } catch (error) { + console.error('搜索用户失败:', error); + throw error; + } + } + + // 🔥 ===== 通知管理(轮询降级) ===== + + // 拉取好友通知 + async pullFriendNotifications(limit = 10) { + try { + const response = await this.apiClient.get('/api/v1/social/friend/notifications/pull', { + limit: limit + }); + return response; + } catch (error) { + console.error('拉取好友通知失败:', error); + throw error; + } + } + + // 获取通知统计 + async getNotificationStats() { + try { + const response = await this.apiClient.get('/api/v1/social/friend/notifications/stats'); + return response; + } catch (error) { + console.error('获取通知统计失败:', error); + throw error; + } + } + + // 清除通知缓存 + async clearNotificationCache() { + try { + const response = await this.apiClient.delete('/api/v1/social/friend/notifications/clear'); + return response; + } catch (error) { + console.error('清除通知缓存失败:', error); + throw error; + } + } + + // 🔥 ===== 工具方法 ===== + + // 格式化关系状态文本 + getRelationStatusText(relationStatus) { + const statusMap = { + 'self': '自己', + 'friend': '已是好友', + 'can_add': '可以添加好友', + 'pending': '等待验证', + 'privacy_limited': '隐私限制', + 'blocked': '被拉黑' + }; + return statusMap[relationStatus] || '未知状态'; + } + + // 格式化搜索类型文本 + getSearchTypeText(searchType) { + const typeMap = { + 'nickname': '昵称', + 'custom_id': '用户ID', + 'phone': '手机号', + 'all': '全部' + }; + return typeMap[searchType] || '未知类型'; + } + + // 格式化隐私级别文本 + getPrivacyLevelText(privacyLevel) { + const levelMap = { + 1: '精确位置', + 2: '区县级别', + 3: '城市级别', + 4: '不可见' + }; + return levelMap[privacyLevel] || '未知级别'; + } + + // 格式化关系标签 + getRelationLabelText(relation) { + const relationMap = { + '情侣': '💕 情侣', + '家人': '👨‍👩‍👧‍👦 家人', + '兄弟': '👬 兄弟', + '姐妹': '👭 姐妹', + '闺蜜': '👯‍♀️ 闺蜜', + '死党': '🤝 死党' + }; + return relationMap[relation] || relation; + } + + // 计算友谊天数 + calculateFriendshipDays(friendshipDate) { + if (!friendshipDate) return 0; + + const now = new Date(); + const friendDate = new Date(friendshipDate); + const diffTime = Math.abs(now - friendDate); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + return diffDays; + } + + // 格式化友谊时间描述 + formatFriendshipTime(friendshipDate) { + if (!friendshipDate) return '未知时间'; + + const days = this.calculateFriendshipDays(friendshipDate); + + if (days < 1) { + return '今天成为好友'; + } else if (days < 30) { + return `${days}天前成为好友`; + } else if (days < 365) { + const months = Math.floor(days / 30); + return `${months}个月前成为好友`; + } else { + const years = Math.floor(days / 365); + return `${years}年前成为好友`; + } + } + + // 验证CustomID格式 + validateCustomId(customId) { + if (!customId || typeof customId !== 'string') { + return false; + } + + // CustomID应该是9-10位数字字符串 + const customIdRegex = /^\d{9,10}$/; + return customIdRegex.test(customId); + } + + // 脱敏手机号 + maskPhoneNumber(phone) { + if (!phone || phone.length < 11) { + return phone; + } + + return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'); + } + + // 生成好友分组列表 + getFriendGroups() { + return [ + '默认分组', + '家人', + '同学', + '同事', + '朋友', + '其他' + ]; + } + + // 生成关系标签列表 + getRelationLabels() { + return [ + { value: '情侣', label: '💕 情侣', color: '#ff4757' }, + { value: '家人', label: '👨‍👩‍👧‍👦 家人', color: '#2ed573' }, + { value: '兄弟', label: '👬 兄弟', color: '#3742fa' }, + { value: '姐妹', label: '👭 姐妹', color: '#ff6b81' }, + { value: '闺蜜', label: '👯‍♀️ 闺蜜', color: '#a55eea' }, + { value: '死党', label: '🤝 死党', color: '#26de81' } + ]; + } +} + +// 创建全局单例 +const friendAPI = new FriendAPI(); + +module.exports = friendAPI; diff --git a/utils/group-api.js b/utils/group-api.js new file mode 100644 index 0000000..2e14d66 --- /dev/null +++ b/utils/group-api.js @@ -0,0 +1,331 @@ +// 群组功能API客户端 +const apiClient = require('./api-client.js'); + +class GroupAPI { + constructor() { + this.apiClient = apiClient; + } + + // 🔥 ===== 群组管理 ===== + + // 创建群组 + async createGroup(groupData) { + try { + const response = await this.apiClient.post('/api/v1/group/create', { + name: groupData.name, + avatar: groupData.avatar || '', + description: groupData.description || '', + notice: groupData.notice || '', + type: groupData.type || 0, + joinType: groupData.joinType || 0, + memberIds: groupData.memberIds || [] + }); + return response; + } catch (error) { + console.error('创建群组失败:', error); + throw error; + } + } + + // 获取用户群组列表 + async getGroupList() { + try { + const response = await this.apiClient.get('/api/v1/group/list'); + return response; + } catch (error) { + console.error('获取群组列表失败:', error); + throw error; + } + } + + // 获取群组详情 + async getGroupDetail(groupId) { + try { + const response = await this.apiClient.get(`/api/v1/group/${groupId}/detail`); + return response; + } catch (error) { + console.error('获取群组详情失败:', error); + throw error; + } + } + + // 获取群组信息 + async getGroupInfo(groupId) { + try { + const response = await this.apiClient.get(`/api/v1/group/${groupId}/info`); + return response; + } catch (error) { + console.error('获取群组信息失败:', error); + throw error; + } + } + + // 更新群组信息 + async updateGroup(groupId, updates) { + try { + const response = await this.apiClient.put(`/api/v1/group/${groupId}`, updates); + return response; + } catch (error) { + console.error('更新群组信息失败:', error); + throw error; + } + } + + // 删除群组 + async deleteGroup(groupId) { + try { + const response = await this.apiClient.delete(`/api/v1/group/${groupId}`); + return response; + } catch (error) { + console.error('删除群组失败:', error); + throw error; + } + } + + // 申请加入群组 + async joinGroup(groupId, message = '') { + try { + const response = await this.apiClient.post('/api/v1/group/join', { + groupId: groupId, + message: message + }); + return response; + } catch (error) { + console.error('申请加入群组失败:', error); + throw error; + } + } + + // 通过邀请码加入群组 + async joinGroupByCode(groupCode, message = '') { + try { + const response = await this.apiClient.post('/api/v1/group/join-by-code', { + groupCode: groupCode, + message: message + }); + return response; + } catch (error) { + console.error('通过邀请码加入群组失败:', error); + throw error; + } + } + + // 退出群组 + async leaveGroup(groupId) { + try { + const response = await this.apiClient.post(`/api/v1/group/${groupId}/leave`); + return response; + } catch (error) { + console.error('退出群组失败:', error); + throw error; + } + } + + // 🔥 ===== 群成员管理 ===== + + // 获取群成员列表 + async getGroupMembers(groupId) { + try { + const response = await this.apiClient.get(`/api/v1/group/${groupId}/members`); + return response; + } catch (error) { + console.error('获取群成员列表失败:', error); + throw error; + } + } + + // 添加群成员 + async addGroupMembers(groupId, memberIds) { + try { + const response = await this.apiClient.post(`/api/v1/group/${groupId}/members`, { + memberIds: memberIds + }); + return response; + } catch (error) { + console.error('添加群成员失败:', error); + throw error; + } + } + + // 移除群成员 + async removeGroupMember(groupId, memberId) { + try { + const response = await this.apiClient.delete(`/api/v1/group/${groupId}/members/${memberId}`); + return response; + } catch (error) { + console.error('移除群成员失败:', error); + throw error; + } + } + + // 更新群成员信息 + async updateGroupMember(memberData) { + try { + const response = await this.apiClient.put('/api/v1/group/members', memberData); + return response; + } catch (error) { + console.error('更新群成员信息失败:', error); + throw error; + } + } + + // 🔥 ===== 群组加入请求管理 ===== + + // 获取群组加入请求 + async getGroupJoinRequests(groupId) { + try { + const response = await this.apiClient.get(`/api/v1/group/${groupId}/join-requests`); + return response; + } catch (error) { + console.error('获取群组加入请求失败:', error); + throw error; + } + } + + // 处理群组加入请求 + async handleGroupJoinRequest(requestData) { + try { + const response = await this.apiClient.post('/api/v1/group/join-requests', requestData); + return response; + } catch (error) { + console.error('处理群组加入请求失败:', error); + throw error; + } + } + + // 🔥 ===== 工具方法 ===== + + // 格式化群组类型 + getGroupTypeText(type) { + const typeMap = { + 0: '普通群', + 1: '频道' + }; + return typeMap[type] || '未知类型'; + } + + // 格式化加入方式 + getJoinTypeText(joinType) { + const joinTypeMap = { + 0: '自由加入', + 1: '需要验证', + 2: '禁止加入' + }; + return joinTypeMap[joinType] || '未知方式'; + } + + // 格式化群组状态 + getGroupStatusText(status) { + const statusMap = { + 0: '已解散', + 1: '正常', + 2: '已封禁' + }; + return statusMap[status] || '未知状态'; + } + + // 格式化成员角色 + getMemberRoleText(role) { + const roleMap = { + 0: '普通成员', + 1: '管理员', + 2: '群主' + }; + return roleMap[role] || '未知角色'; + } + + // 验证群组名称 + validateGroupName(name) { + if (!name || typeof name !== 'string') { + return { valid: false, message: '群组名称不能为空' }; + } + + if (name.length < 2) { + return { valid: false, message: '群组名称至少2个字符' }; + } + + if (name.length > 50) { + return { valid: false, message: '群组名称不能超过50个字符' }; + } + + return { valid: true, message: '' }; + } + + // 验证群组描述 + validateGroupDescription(description) { + if (!description) { + return { valid: true, message: '' }; + } + + if (description.length > 200) { + return { valid: false, message: '群组描述不能超过200个字符' }; + } + + return { valid: true, message: '' }; + } + + // 验证群组公告 + validateGroupNotice(notice) { + if (!notice) { + return { valid: true, message: '' }; + } + + if (notice.length > 200) { + return { valid: false, message: '群组公告不能超过200个字符' }; + } + + return { valid: true, message: '' }; + } + + // 验证邀请码格式 + validateGroupCode(groupCode) { + if (!groupCode || typeof groupCode !== 'string') { + return false; + } + + // 邀请码应该是8-20位字符 + const codeRegex = /^[A-Z0-9]{8,20}$/; + return codeRegex.test(groupCode); + } + + // 生成群组默认头像 + generateGroupAvatar(groupName) { + // 可以根据群组名称生成默认头像URL + const firstChar = groupName ? groupName.charAt(0) : 'G'; + return `https://ui-avatars.com/api/?name=${encodeURIComponent(firstChar)}&background=random&color=fff&size=200`; + } + + // 计算群组成员上限 + getGroupMemberLimit(type, ownerMemberLevel = 0) { + if (type === 1) { + // 频道类型 + return ownerMemberLevel >= 2 ? 10000 : 5000; + } else { + // 普通群 + const limits = { + 0: 100, // 普通用户 + 1: 200, // 会员 + 2: 500, // 高级会员 + 3: 1000 // VIP会员 + }; + return limits[ownerMemberLevel] || 100; + } + } + + // 检查用户权限 + checkUserPermission(userRole, action) { + const permissions = { + 0: ['view', 'send_message'], // 普通成员 + 1: ['view', 'send_message', 'manage_members', 'edit_info'], // 管理员 + 2: ['view', 'send_message', 'manage_members', 'edit_info', 'delete_group', 'manage_admins'] // 群主 + }; + + const userPermissions = permissions[userRole] || []; + return userPermissions.includes(action); + } +} + +// 创建全局单例 +const groupAPI = new GroupAPI(); + +module.exports = groupAPI; diff --git a/utils/group-chat-manager.js b/utils/group-chat-manager.js new file mode 100644 index 0000000..01dbaa3 --- /dev/null +++ b/utils/group-chat-manager.js @@ -0,0 +1,699 @@ +// 群聊管理器 - 微信小程序专用 +// 处理群聊创建、管理、成员管理、权限控制等功能 + +const apiClient = require('./api-client.js'); + +/** + * 群聊管理器 + * 功能: + * 1. 群聊创建和解散 + * 2. 群成员管理 + * 3. 群信息设置 + * 4. 群权限管理 + * 5. @提醒功能 + * 6. 群公告管理 + */ +class GroupChatManager { + constructor() { + this.isInitialized = false; + + // 群聊配置 + this.groupConfig = { + // 群聊基本配置 + maxMembers: 500, // 最大成员数 + maxGroupNameLength: 20, // 群名称最大长度 + maxDescriptionLength: 200, // 群描述最大长度 + maxAnnouncementLength: 500, // 群公告最大长度 + + // 权限配置 + permissions: { + owner: { + name: '群主', + level: 3, + canInvite: true, + canRemove: true, + canSetAdmin: true, + canEditInfo: true, + canDissolve: true, + canMute: true, + canSetAnnouncement: true + }, + admin: { + name: '管理员', + level: 2, + canInvite: true, + canRemove: true, + canSetAdmin: false, + canEditInfo: true, + canDissolve: false, + canMute: true, + canSetAnnouncement: true + }, + member: { + name: '普通成员', + level: 1, + canInvite: false, + canRemove: false, + canSetAdmin: false, + canEditInfo: false, + canDissolve: false, + canMute: false, + canSetAnnouncement: false + } + }, + + // @提醒配置 + mention: { + enabled: true, + maxMentions: 10, // 单条消息最多@10个人 + allMemberKeyword: '所有人' // @全体成员关键词 + }, + + // 群设置默认值 + defaultSettings: { + allowMemberInvite: true, // 允许成员邀请 + allowMemberModifyInfo: false, // 允许成员修改群信息 + muteAll: false, // 全员禁言 + showMemberNickname: true, // 显示成员昵称 + saveToContacts: true, // 保存到通讯录 + showQRCode: true // 显示群二维码 + } + }; + + // 群聊缓存 + this.groupCache = new Map(); + this.memberCache = new Map(); + + // 当前用户信息 + this.currentUserId = null; + + this.init(); + } + + // 初始化群聊管理器 + async init() { + if (this.isInitialized) return; + + console.log('👥 初始化群聊管理器...'); + + try { + // 获取当前用户ID + this.currentUserId = wx.getStorageSync('userId'); + + // 加载群聊缓存 + await this.loadGroupCache(); + + this.isInitialized = true; + console.log('✅ 群聊管理器初始化完成'); + + } catch (error) { + console.error('❌ 群聊管理器初始化失败:', error); + } + } + + // 👥 ===== 群聊创建和管理 ===== + + // 创建群聊 + async createGroup(groupInfo) { + try { + console.log('👥 创建群聊:', groupInfo); + + // 验证群聊信息 + const validation = this.validateGroupInfo(groupInfo); + if (!validation.valid) { + return { success: false, error: validation.error }; + } + + // 调用API创建群聊 + const response = await apiClient.request({ + url: '/api/v1/groups', + method: 'POST', + data: { + name: groupInfo.name, + description: groupInfo.description || '', + avatar: groupInfo.avatar || '', + memberIds: groupInfo.memberIds || [], + settings: { ...this.groupConfig.defaultSettings, ...groupInfo.settings } + } + }); + + if (response.success) { + const group = response.data; + + // 更新本地缓存 + this.updateGroupCache(group); + + console.log('✅ 群聊创建成功:', group.groupId); + return { success: true, data: group }; + } else { + throw new Error(response.error || '创建群聊失败'); + } + + } catch (error) { + console.error('❌ 创建群聊失败:', error); + return { success: false, error: error.message }; + } + } + + // 解散群聊 + async dissolveGroup(groupId) { + try { + console.log('👥 解散群聊:', groupId); + + // 检查权限 + const hasPermission = await this.checkPermission(groupId, 'canDissolve'); + if (!hasPermission) { + return { success: false, error: '没有解散群聊的权限' }; + } + + // 调用API解散群聊 + const response = await apiClient.request({ + url: `/api/v1/groups/${groupId}/dissolve`, + method: 'POST' + }); + + if (response.success) { + // 清除本地缓存 + this.groupCache.delete(groupId); + this.memberCache.delete(groupId); + + console.log('✅ 群聊解散成功'); + return { success: true }; + } else { + throw new Error(response.error || '解散群聊失败'); + } + + } catch (error) { + console.error('❌ 解散群聊失败:', error); + return { success: false, error: error.message }; + } + } + + // 退出群聊 + async leaveGroup(groupId) { + try { + console.log('👥 退出群聊:', groupId); + + // 调用API退出群聊 + const response = await apiClient.request({ + url: `/api/v1/groups/${groupId}/leave`, + method: 'POST' + }); + + if (response.success) { + // 清除本地缓存 + this.groupCache.delete(groupId); + this.memberCache.delete(groupId); + + console.log('✅ 退出群聊成功'); + return { success: true }; + } else { + throw new Error(response.error || '退出群聊失败'); + } + + } catch (error) { + console.error('❌ 退出群聊失败:', error); + return { success: false, error: error.message }; + } + } + + // 👤 ===== 群成员管理 ===== + + // 邀请成员 + async inviteMembers(groupId, memberIds) { + try { + console.log('👤 邀请成员:', groupId, memberIds); + + // 检查权限 + const hasPermission = await this.checkPermission(groupId, 'canInvite'); + if (!hasPermission) { + return { success: false, error: '没有邀请成员的权限' }; + } + + // 检查成员数量限制 + const currentMembers = await this.getGroupMembers(groupId); + if (currentMembers.data && currentMembers.data.length + memberIds.length > this.groupConfig.maxMembers) { + return { success: false, error: `群成员数量不能超过${this.groupConfig.maxMembers}人` }; + } + + // 调用API邀请成员 + const response = await apiClient.request({ + url: `/api/v1/groups/${groupId}/members`, + method: 'POST', + data: { + memberIds: memberIds + } + }); + + if (response.success) { + // 更新成员缓存 + this.updateMemberCache(groupId, response.data.members); + + console.log('✅ 邀请成员成功'); + return { success: true, data: response.data }; + } else { + throw new Error(response.error || '邀请成员失败'); + } + + } catch (error) { + console.error('❌ 邀请成员失败:', error); + return { success: false, error: error.message }; + } + } + + // 移除成员 + async removeMember(groupId, memberId) { + try { + console.log('👤 移除成员:', groupId, memberId); + + // 检查权限 + const hasPermission = await this.checkPermission(groupId, 'canRemove'); + if (!hasPermission) { + return { success: false, error: '没有移除成员的权限' }; + } + + // 不能移除自己 + if (memberId === this.currentUserId) { + return { success: false, error: '不能移除自己,请使用退出群聊功能' }; + } + + // 调用API移除成员 + const response = await apiClient.request({ + url: `/api/v1/groups/${groupId}/members/${memberId}`, + method: 'DELETE' + }); + + if (response.success) { + // 更新成员缓存 + this.removeMemberFromCache(groupId, memberId); + + console.log('✅ 移除成员成功'); + return { success: true }; + } else { + throw new Error(response.error || '移除成员失败'); + } + + } catch (error) { + console.error('❌ 移除成员失败:', error); + return { success: false, error: error.message }; + } + } + + // 设置管理员 + async setAdmin(groupId, memberId, isAdmin) { + try { + console.log('👤 设置管理员:', groupId, memberId, isAdmin); + + // 检查权限 + const hasPermission = await this.checkPermission(groupId, 'canSetAdmin'); + if (!hasPermission) { + return { success: false, error: '没有设置管理员的权限' }; + } + + // 调用API设置管理员 + const response = await apiClient.request({ + url: `/api/v1/groups/${groupId}/members/${memberId}/admin`, + method: 'PUT', + data: { + isAdmin: isAdmin + } + }); + + if (response.success) { + // 更新成员缓存 + this.updateMemberRole(groupId, memberId, isAdmin ? 'admin' : 'member'); + + console.log('✅ 设置管理员成功'); + return { success: true }; + } else { + throw new Error(response.error || '设置管理员失败'); + } + + } catch (error) { + console.error('❌ 设置管理员失败:', error); + return { success: false, error: error.message }; + } + } + + // 获取群成员列表 + async getGroupMembers(groupId, useCache = true) { + try { + // 先从缓存获取 + if (useCache && this.memberCache.has(groupId)) { + const cached = this.memberCache.get(groupId); + if (Date.now() - cached.timestamp < 300000) { // 5分钟缓存 + return { success: true, data: cached.members }; + } + } + + // 从服务器获取 + const response = await apiClient.request({ + url: `/api/v1/groups/${groupId}/members`, + method: 'GET' + }); + + if (response.success) { + const members = response.data || []; + + // 更新缓存 + this.memberCache.set(groupId, { + members: members, + timestamp: Date.now() + }); + + return { success: true, data: members }; + } else { + throw new Error(response.error || '获取群成员失败'); + } + + } catch (error) { + console.error('❌ 获取群成员失败:', error); + return { success: false, error: error.message }; + } + } + + // ⚙️ ===== 群信息设置 ===== + + // 更新群信息 + async updateGroupInfo(groupId, updates) { + try { + console.log('⚙️ 更新群信息:', groupId, updates); + + // 检查权限 + const hasPermission = await this.checkPermission(groupId, 'canEditInfo'); + if (!hasPermission) { + return { success: false, error: '没有修改群信息的权限' }; + } + + // 验证更新信息 + const validation = this.validateGroupUpdates(updates); + if (!validation.valid) { + return { success: false, error: validation.error }; + } + + // 调用API更新群信息 + const response = await apiClient.request({ + url: `/api/v1/groups/${groupId}`, + method: 'PUT', + data: updates + }); + + if (response.success) { + // 更新群聊缓存 + this.updateGroupCache(response.data); + + console.log('✅ 群信息更新成功'); + return { success: true, data: response.data }; + } else { + throw new Error(response.error || '更新群信息失败'); + } + + } catch (error) { + console.error('❌ 更新群信息失败:', error); + return { success: false, error: error.message }; + } + } + + // 设置群公告 + async setGroupAnnouncement(groupId, announcement) { + try { + console.log('⚙️ 设置群公告:', groupId); + + // 检查权限 + const hasPermission = await this.checkPermission(groupId, 'canSetAnnouncement'); + if (!hasPermission) { + return { success: false, error: '没有设置群公告的权限' }; + } + + // 验证公告长度 + if (announcement.length > this.groupConfig.maxAnnouncementLength) { + return { success: false, error: `群公告不能超过${this.groupConfig.maxAnnouncementLength}字符` }; + } + + // 调用API设置群公告 + const response = await apiClient.request({ + url: `/api/v1/groups/${groupId}/announcement`, + method: 'PUT', + data: { + announcement: announcement + } + }); + + if (response.success) { + console.log('✅ 群公告设置成功'); + return { success: true }; + } else { + throw new Error(response.error || '设置群公告失败'); + } + + } catch (error) { + console.error('❌ 设置群公告失败:', error); + return { success: false, error: error.message }; + } + } + + // 📢 ===== @提醒功能 ===== + + // 解析@提醒 + parseMentions(content) { + const mentions = []; + const mentionRegex = /@([^\s@]+)/g; + let match; + + while ((match = mentionRegex.exec(content)) !== null) { + const mentionText = match[1]; + + if (mentionText === this.groupConfig.mention.allMemberKeyword) { + // @全体成员 + mentions.push({ + type: 'all', + text: mentionText, + start: match.index, + end: match.index + match[0].length + }); + } else { + // @特定成员 + mentions.push({ + type: 'user', + text: mentionText, + start: match.index, + end: match.index + match[0].length + }); + } + } + + return mentions; + } + + // 验证@提醒 + async validateMentions(groupId, mentions) { + try { + if (mentions.length > this.groupConfig.mention.maxMentions) { + return { valid: false, error: `单条消息最多只能@${this.groupConfig.mention.maxMentions}个人` }; + } + + // 获取群成员列表 + const membersResult = await this.getGroupMembers(groupId); + if (!membersResult.success) { + return { valid: false, error: '获取群成员列表失败' }; + } + + const members = membersResult.data; + const validMentions = []; + + for (const mention of mentions) { + if (mention.type === 'all') { + // 检查@全体成员权限 + const hasPermission = await this.checkPermission(groupId, 'canMentionAll'); + if (hasPermission) { + validMentions.push({ + ...mention, + userIds: members.map(m => m.userId) + }); + } + } else { + // 查找对应的成员 + const member = members.find(m => + m.nickname === mention.text || + m.username === mention.text || + m.userId === mention.text + ); + + if (member) { + validMentions.push({ + ...mention, + userId: member.userId, + userIds: [member.userId] + }); + } + } + } + + return { valid: true, mentions: validMentions }; + + } catch (error) { + console.error('❌ 验证@提醒失败:', error); + return { valid: false, error: '验证@提醒失败' }; + } + } + + // 🔧 ===== 工具方法 ===== + + // 验证群聊信息 + validateGroupInfo(groupInfo) { + if (!groupInfo.name || groupInfo.name.trim().length === 0) { + return { valid: false, error: '群名称不能为空' }; + } + + if (groupInfo.name.length > this.groupConfig.maxGroupNameLength) { + return { valid: false, error: `群名称不能超过${this.groupConfig.maxGroupNameLength}字符` }; + } + + if (groupInfo.description && groupInfo.description.length > this.groupConfig.maxDescriptionLength) { + return { valid: false, error: `群描述不能超过${this.groupConfig.maxDescriptionLength}字符` }; + } + + if (groupInfo.memberIds && groupInfo.memberIds.length > this.groupConfig.maxMembers) { + return { valid: false, error: `群成员数量不能超过${this.groupConfig.maxMembers}人` }; + } + + return { valid: true }; + } + + // 验证群信息更新 + validateGroupUpdates(updates) { + if (updates.name !== undefined) { + if (!updates.name || updates.name.trim().length === 0) { + return { valid: false, error: '群名称不能为空' }; + } + + if (updates.name.length > this.groupConfig.maxGroupNameLength) { + return { valid: false, error: `群名称不能超过${this.groupConfig.maxGroupNameLength}字符` }; + } + } + + if (updates.description !== undefined && updates.description.length > this.groupConfig.maxDescriptionLength) { + return { valid: false, error: `群描述不能超过${this.groupConfig.maxDescriptionLength}字符` }; + } + + return { valid: true }; + } + + // 检查权限 + async checkPermission(groupId, permission) { + try { + const userRole = await this.getUserRole(groupId, this.currentUserId); + if (!userRole) return false; + + const roleConfig = this.groupConfig.permissions[userRole]; + return roleConfig && roleConfig[permission]; + + } catch (error) { + console.error('❌ 检查权限失败:', error); + return false; + } + } + + // 获取用户在群中的角色 + async getUserRole(groupId, userId) { + try { + const membersResult = await this.getGroupMembers(groupId); + if (!membersResult.success) return null; + + const member = membersResult.data.find(m => m.userId === userId); + return member ? member.role : null; + + } catch (error) { + console.error('❌ 获取用户角色失败:', error); + return null; + } + } + + // 更新群聊缓存 + updateGroupCache(group) { + this.groupCache.set(group.groupId, { + ...group, + timestamp: Date.now() + }); + } + + // 更新成员缓存 + updateMemberCache(groupId, members) { + this.memberCache.set(groupId, { + members: members, + timestamp: Date.now() + }); + } + + // 从缓存中移除成员 + removeMemberFromCache(groupId, memberId) { + const cached = this.memberCache.get(groupId); + if (cached) { + cached.members = cached.members.filter(m => m.userId !== memberId); + this.memberCache.set(groupId, cached); + } + } + + // 更新成员角色 + updateMemberRole(groupId, memberId, role) { + const cached = this.memberCache.get(groupId); + if (cached) { + const member = cached.members.find(m => m.userId === memberId); + if (member) { + member.role = role; + this.memberCache.set(groupId, cached); + } + } + } + + // 加载群聊缓存 + async loadGroupCache() { + try { + const cached = wx.getStorageSync('groupCache') || {}; + this.groupCache = new Map(Object.entries(cached)); + } catch (error) { + console.error('❌ 加载群聊缓存失败:', error); + } + } + + // 保存群聊缓存 + saveGroupCache() { + try { + const cacheObj = Object.fromEntries(this.groupCache); + wx.setStorageSync('groupCache', cacheObj); + } catch (error) { + console.error('❌ 保存群聊缓存失败:', error); + } + } + + // 获取群聊管理器状态 + getStatus() { + return { + isInitialized: this.isInitialized, + groupCount: this.groupCache.size, + memberCacheCount: this.memberCache.size, + currentUserId: this.currentUserId, + config: this.groupConfig + }; + } + + // 清除所有缓存 + clearAllCache() { + this.groupCache.clear(); + this.memberCache.clear(); + wx.removeStorageSync('groupCache'); + console.log('👥 已清除所有群聊缓存'); + } + + // 重置管理器 + reset() { + this.clearAllCache(); + this.currentUserId = null; + this.isInitialized = false; + } +} + +// 创建全局实例 +const groupChatManager = new GroupChatManager(); + +module.exports = groupChatManager; diff --git a/utils/image-cache-manager.js b/utils/image-cache-manager.js new file mode 100644 index 0000000..45f00e1 --- /dev/null +++ b/utils/image-cache-manager.js @@ -0,0 +1,283 @@ +/** + * 图片缓存管理器 + * 实现头像和聊天图片的缓存功能 + */ + +class ImageCacheManager { + constructor() { + this.cache = new Map(); + this.cacheExpiry = new Map(); + this.maxCacheSize = 100; // 最大缓存数量 + this.cacheExpiryTime = 7 * 24 * 60 * 60 * 1000; // 7天缓存过期时间 + this.avatarCacheExpiryTime = 30 * 24 * 60 * 60 * 1000; // 头像30天缓存过期时间 + + // 初始化缓存 + this.initCache(); + } + + /** + * 初始化缓存,从本地存储加载 + */ + initCache() { + try { + const cachedData = wx.getStorageSync('image_cache'); + if (cachedData) { + const data = JSON.parse(cachedData); + this.cache = new Map(data.cache || []); + this.cacheExpiry = new Map(data.expiry || []); + console.log('📦 图片缓存初始化完成,缓存数量:', this.cache.size); + } + } catch (error) { + console.error('❌ 初始化图片缓存失败:', error); + } + } + + /** + * 保存缓存到本地存储 + */ + saveCache() { + try { + const data = { + cache: Array.from(this.cache.entries()), + expiry: Array.from(this.cacheExpiry.entries()) + }; + wx.setStorageSync('image_cache', JSON.stringify(data)); + } catch (error) { + console.error('❌ 保存图片缓存失败:', error); + } + } + + /** + * 清理过期缓存 + */ + cleanExpiredCache() { + const now = Date.now(); + const expiredKeys = []; + + for (const [key, expiryTime] of this.cacheExpiry.entries()) { + if (now > expiryTime) { + expiredKeys.push(key); + } + } + + expiredKeys.forEach(key => { + this.cache.delete(key); + this.cacheExpiry.delete(key); + }); + + if (expiredKeys.length > 0) { + console.log('🧹 清理过期缓存:', expiredKeys.length, '个'); + this.saveCache(); + } + } + + /** + * 清理超出最大数量的缓存 + */ + cleanExcessCache() { + if (this.cache.size <= this.maxCacheSize) return; + + // 按过期时间排序,删除最旧的 + const sortedEntries = Array.from(this.cacheExpiry.entries()) + .sort((a, b) => a[1] - b[1]); + + const excessCount = this.cache.size - this.maxCacheSize; + const keysToDelete = sortedEntries.slice(0, excessCount).map(([key]) => key); + + keysToDelete.forEach(key => { + this.cache.delete(key); + this.cacheExpiry.delete(key); + }); + + console.log('🧹 清理超出限制的缓存:', keysToDelete.length, '个'); + this.saveCache(); + } + + /** + * 生成缓存键 + */ + generateCacheKey(url, type = 'image') { + return `${type}_${url}`; + } + + /** + * 检查缓存是否存在且有效 + */ + isCached(url, type = 'image') { + const key = this.generateCacheKey(url, type); + const expiryTime = this.cacheExpiry.get(key); + + if (!expiryTime) return false; + + if (Date.now() > expiryTime) { + // 缓存已过期,删除 + this.cache.delete(key); + this.cacheExpiry.delete(key); + return false; + } + + return this.cache.has(key); + } + + /** + * 获取缓存的图片路径 + */ + getCachedPath(url, type = 'image') { + const key = this.generateCacheKey(url, type); + return this.cache.get(key); + } + + /** + * 缓存图片 + */ + async cacheImage(url, type = 'image') { + if (!url || url.startsWith('data:') || url.startsWith('/')) { + return url; // 不缓存base64、本地路径等 + } + + const key = this.generateCacheKey(url, type); + + // 检查是否已缓存 + if (this.isCached(url, type)) { + console.log('📦 使用缓存图片:', url); + return this.getCachedPath(url, type); + } + + try { + console.log('📥 开始缓存图片:', url); + + // 下载图片 + const downloadResult = await new Promise((resolve, reject) => { + wx.downloadFile({ + url: url, + success: resolve, + fail: reject + }); + }); + + if (downloadResult.statusCode === 200) { + const localPath = downloadResult.tempFilePath; + + // 设置缓存过期时间 + const expiryTime = type === 'avatar' + ? Date.now() + this.avatarCacheExpiryTime + : Date.now() + this.cacheExpiryTime; + + // 保存到缓存 + this.cache.set(key, localPath); + this.cacheExpiry.set(key, expiryTime); + + // 清理过期和超量缓存 + this.cleanExpiredCache(); + this.cleanExcessCache(); + + console.log('✅ 图片缓存成功:', url, '->', localPath); + return localPath; + } else { + console.warn('⚠️ 图片下载失败,状态码:', downloadResult.statusCode); + return url; + } + } catch (error) { + console.error('❌ 缓存图片失败:', url, error); + return url; + } + } + + /** + * 缓存头像 + */ + async cacheAvatar(url) { + return this.cacheImage(url, 'avatar'); + } + + /** + * 更新头像缓存(当头像URL变化时) + */ + async updateAvatarCache(oldUrl, newUrl) { + if (oldUrl && oldUrl !== newUrl) { + const oldKey = this.generateCacheKey(oldUrl, 'avatar'); + this.cache.delete(oldKey); + this.cacheExpiry.delete(oldKey); + console.log('🔄 更新头像缓存:', oldUrl, '->', newUrl); + } + + if (newUrl) { + return await this.cacheAvatar(newUrl); + } + + return newUrl; + } + + /** + * 预加载图片(用于聊天中的图片) + */ + async preloadImage(url) { + return this.cacheImage(url, 'chat_image'); + } + + /** + * 获取缓存统计信息 + */ + getCacheStats() { + const now = Date.now(); + let avatarCount = 0; + let imageCount = 0; + let expiredCount = 0; + + for (const [key, expiryTime] of this.cacheExpiry.entries()) { + if (key.startsWith('avatar_')) { + avatarCount++; + } else if (key.startsWith('image_') || key.startsWith('chat_image_')) { + imageCount++; + } + + if (now > expiryTime) { + expiredCount++; + } + } + + return { + total: this.cache.size, + avatar: avatarCount, + image: imageCount, + expired: expiredCount, + maxSize: this.maxCacheSize + }; + } + + /** + * 清空所有缓存 + */ + clearAllCache() { + this.cache.clear(); + this.cacheExpiry.clear(); + this.saveCache(); + console.log('🗑️ 清空所有图片缓存'); + } + + /** + * 清理指定类型的缓存 + */ + clearCacheByType(type) { + const keysToDelete = []; + + for (const key of this.cache.keys()) { + if (key.startsWith(`${type}_`)) { + keysToDelete.push(key); + } + } + + keysToDelete.forEach(key => { + this.cache.delete(key); + this.cacheExpiry.delete(key); + }); + + console.log(`🗑️ 清理${type}类型缓存:`, keysToDelete.length, '个'); + this.saveCache(); + } +} + +// 创建单例实例 +const imageCacheManager = new ImageCacheManager(); + +module.exports = imageCacheManager; \ No newline at end of file diff --git a/utils/map-config.js b/utils/map-config.js new file mode 100644 index 0000000..e9f160e --- /dev/null +++ b/utils/map-config.js @@ -0,0 +1,336 @@ +/** + * 地图配置文件 + * 包含高德地图配置、权限管理、地图工具函数等 + */ + +const config = require('../config/config.js'); + +// 高德地图配置 +const MAP_CONFIG = { + // 高德地图Key - 需要在高德开放平台申请 + amapKey: config.amapKey || '9281427fb1b9c4e1c2acf097a3194781', + + // 地图默认设置 + defaults: { + latitude: 39.908823, // 北京天安门 + longitude: 116.39747, + scale: 16, + minScale: 3, + maxScale: 20, + showLocation: true, + showScale: true, + showCompass: true, + enableOverlooking: false, + enableZoom: true, + enableScroll: true, + enableRotate: false, + enable3D: false + }, + + // 定位配置 + location: { + type: 'gcj02', // 坐标系类型:wgs84、gcj02 + isHighAccuracy: true, + highAccuracyExpireTime: 4000, + timeout: 10000, + cacheTimeout: 60000 // 位置缓存时间 + }, + + // 标记点配置 + markers: { + // 当前用户标记 - 使用默认样式 + currentUser: { + width: 30, + height: 30, + anchor: { x: 0.5, y: 1 } + }, + // 好友标记 - 使用默认样式 + friend: { + width: 25, + height: 25, + anchor: { x: 0.5, y: 1 } + }, + // 陌生人标记 - 使用默认样式 + stranger: { + width: 20, + height: 20, + anchor: { x: 0.5, y: 1 } + } + }, + + // 地图样式 + style: { + normal: 'normal', // 普通地图 + satellite: 'satellite', // 卫星地图 + traffic: 'traffic' // 交通地图 + } +}; + +// 权限配置 +const PERMISSION_CONFIG = { + // 位置权限 + location: { + scope: 'scope.userLocation', + name: '位置信息', + description: '用于获取您的位置信息,实现定位和附近功能' + }, + + // 其他权限 + camera: { + scope: 'scope.camera', + name: '摄像头', + description: '用于拍照和扫码功能' + }, + + album: { + scope: 'scope.album', + name: '相册', + description: '用于选择图片功能' + } +}; + +/** + * 地图工具类 + */ +class MapUtils { + /** + * 检查位置权限 + */ + static checkLocationPermission() { + return new Promise((resolve, reject) => { + wx.getSetting({ + success: (res) => { + console.log('权限设置:', res.authSetting); + + if (res.authSetting['scope.userLocation'] === false) { + // 用户拒绝过位置权限 + reject({ + type: 'denied', + message: '位置权限被拒绝,请在设置中开启' + }); + } else if (res.authSetting['scope.userLocation'] === true) { + // 用户已授权 + resolve(true); + } else { + // 用户未授权,需要请求授权 + resolve(false); + } + }, + fail: (error) => { + console.error('获取权限设置失败:', error); + reject({ + type: 'error', + message: '获取权限设置失败' + }); + } + }); + }); + } + + /** + * 请求位置权限 + */ + static requestLocationPermission() { + return new Promise((resolve, reject) => { + wx.authorize({ + scope: 'scope.userLocation', + success: () => { + console.log('位置权限授权成功'); + resolve(true); + }, + fail: (error) => { + console.warn('位置权限授权失败:', error); + // 引导用户去设置页面 + wx.showModal({ + title: '位置权限申请', + content: 'FindMe需要访问您的位置信息,请在设置中开启位置权限', + confirmText: '去设置', + cancelText: '取消', + success: (res) => { + if (res.confirm) { + wx.openSetting({ + success: (settingRes) => { + if (settingRes.authSetting['scope.userLocation']) { + resolve(true); + } else { + reject({ + type: 'denied', + message: '位置权限被拒绝' + }); + } + }, + fail: () => { + reject({ + type: 'error', + message: '打开设置页面失败' + }); + } + }); + } else { + reject({ + type: 'cancelled', + message: '用户取消授权' + }); + } + } + }); + } + }); + }); + } + + /** + * 获取位置(带权限检查) + */ + static async getLocation(options = {}) { + try { + // 检查权限 + const hasPermission = await this.checkLocationPermission(); + + if (!hasPermission) { + // 请求权限 + await this.requestLocationPermission(); + } + + // 获取位置 + return new Promise((resolve, reject) => { + const locationOptions = { + ...MAP_CONFIG.location, + ...options, + success: (res) => { + console.log('获取位置成功:', res); + resolve({ + latitude: res.latitude, + longitude: res.longitude, + accuracy: res.accuracy || 0, + altitude: res.altitude || 0, + speed: res.speed || -1, + timestamp: Date.now() + }); + }, + fail: (error) => { + console.error('获取位置失败:', error); + reject({ + type: 'location_error', + message: error.errMsg || '定位失败', + error: error + }); + } + }; + + wx.getLocation(locationOptions); + }); + } catch (error) { + console.error('位置获取流程失败:', error); + throw error; + } + } + + /** + * 计算两点距离 + */ + static calculateDistance(point1, point2) { + const lat1 = point1.latitude * Math.PI / 180; + const lon1 = point1.longitude * Math.PI / 180; + const lat2 = point2.latitude * Math.PI / 180; + const lon2 = point2.longitude * Math.PI / 180; + + const R = 6371000; // 地球半径(米) + const dLat = lat2 - lat1; + const dLon = lon2 - lon1; + + const a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(lat1) * Math.cos(lat2) * + Math.sin(dLon/2) * Math.sin(dLon/2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + + return R * c; + } + + /** + * 格式化距离显示 + */ + static formatDistance(distance) { + if (distance < 1000) { + return `${Math.round(distance)}米`; + } else if (distance < 10000) { + return `${(distance / 1000).toFixed(1)}公里`; + } else { + return `${Math.round(distance / 1000)}公里`; + } + } + + /** + * 创建标记点 + */ + static createMarker(data, type = 'friend') { + const markerConfig = MAP_CONFIG.markers[type] || MAP_CONFIG.markers.friend; + + return { + id: data.id || Date.now(), + latitude: data.latitude, + longitude: data.longitude, + width: markerConfig.width, + height: markerConfig.height, + anchor: markerConfig.anchor, + callout: data.showCallout ? { + content: data.nickname || '用户', + color: '#333333', + fontSize: 12, + borderRadius: 4, + bgColor: '#ffffff', + padding: 8, + display: 'ALWAYS' + } : null, + customData: { + userId: data.userId, + nickname: data.nickname, + avatar: data.avatar, + distance: data.distance, + lastUpdateTime: data.lastUpdateTime + } + }; + } + + /** + * 获取地图区域 + */ + static getMapRegion(centerPoint, markers = []) { + if (markers.length === 0) { + return { + latitude: centerPoint.latitude, + longitude: centerPoint.longitude, + scale: MAP_CONFIG.defaults.scale + }; + } + + // 计算包含所有标记点的区域 + let minLat = centerPoint.latitude; + let maxLat = centerPoint.latitude; + let minLng = centerPoint.longitude; + let maxLng = centerPoint.longitude; + + markers.forEach(marker => { + minLat = Math.min(minLat, marker.latitude); + maxLat = Math.max(maxLat, marker.latitude); + minLng = Math.min(minLng, marker.longitude); + maxLng = Math.max(maxLng, marker.longitude); + }); + + // 添加边距 + const latPadding = (maxLat - minLat) * 0.3; + const lngPadding = (maxLng - minLng) * 0.3; + + return { + latitude: (minLat + maxLat) / 2, + longitude: (minLng + maxLng) / 2, + scale: Math.max(Math.min(18 - Math.log2(Math.max(maxLat - minLat + latPadding, maxLng - minLng + lngPadding) * 111000), 18), 8) + }; + } +} + +module.exports = { + MAP_CONFIG, + PERMISSION_CONFIG, + MapUtils +}; \ No newline at end of file diff --git a/utils/map-setup.md b/utils/map-setup.md new file mode 100644 index 0000000..a79a47c --- /dev/null +++ b/utils/map-setup.md @@ -0,0 +1,118 @@ +# 高德地图小程序集成配置说明 + +## 1. 申请高德地图API Key + +1. 访问高德开放平台:https://lbs.amap.com/dev/ +2. 注册开发者账号并实名认证 +3. 创建应用,选择"微信小程序"平台 +4. 获取API Key,复制到 `config/config.js` 的 `amapKey` 字段 + +## 2. 配置微信小程序合法域名 + +在微信小程序后台(mp.weixin.qq.com)的"开发设置"中添加以下合法域名: + +**request合法域名:** +- https://restapi.amap.com + +**uploadFile合法域名:** +- https://restapi.amap.com + +**downloadFile合法域名:** +- https://restapi.amap.com + +## 3. 位置权限配置 + +app.json 中已配置位置权限: +```json +{ + "permission": { + "scope.userLocation": { + "desc": "你的位置信息将用于小程序位置接口的效果展示" + } + }, + "requiredBackgroundModes": ["location"], + "requiredPrivateInfos": ["getLocation"] +} +``` + +## 4. 使用的功能 + +当前实现的地图功能包括: + +### 基础功能 +- GPS定位获取当前位置 +- 高德地图逆地理编码(经纬度转地址) +- 地图标记显示 +- 位置权限管理 +- 定时位置更新 + +### 高级功能 +- 好友位置显示 +- 附近用户搜索 +- 位置距离计算 +- 地图区域自适应 +- 位置隐私保护 + +### API集成 +- 与后端位置服务完整对接 +- 支持位置上传和好友位置获取 +- 附近用户搜索功能 +- 位置历史记录 + +## 5. 文件结构 + +``` +miniprogram/ +├── libs/ +│ └── amap-wx.js # 高德地图SDK +├── utils/ +│ ├── map-config.js # 地图配置和工具类 +│ └── api-client.js # API客户端(已添加位置相关方法) +├── pages/map/ +│ ├── map.js # 地图页面逻辑 +│ ├── map.wxml # 地图页面模板 +│ └── map.wxss # 地图页面样式 +└── config/ + └── config.js # 配置文件(需要添加amapKey) +``` + +## 6. 注意事项 + +1. **API Key安全**:不要将API Key提交到公开仓库 +2. **权限引导**:首次使用时会引导用户授权位置权限 +3. **网络环境**:高德API需要网络连接,注意处理网络异常 +4. **坐标系**:使用GCJ02坐标系(高德地图标准) +5. **性能优化**:位置更新采用30秒间隔,避免频繁请求 + +## 7. 测试建议 + +1. 真机测试位置权限和定位功能 +2. 测试网络异常情况的处理 +3. 验证好友位置显示是否正常 +4. 检查地图标记和交互功能 + +## 8. 故障排除 + +### 定位失败 +- 检查位置权限是否开启 +- 确认网络连接正常 +- 验证高德API Key是否有效 + +### 地址解析失败 +- 检查合法域名配置 +- 确认API Key额度是否充足 +- 验证网络请求是否被拦截 + +### 地图显示异常 +- 检查坐标数据格式 +- 确认地图组件属性配置 +- 验证标记数据结构是否正确 + +## 9. 扩展功能 + +可以根据需要添加更多功能: +- 路径规划和导航 +- POI搜索 +- 地理围栏 +- 轨迹回放 +- 多种地图图层 \ No newline at end of file diff --git a/utils/media-manager.js b/utils/media-manager.js new file mode 100644 index 0000000..3a001ad --- /dev/null +++ b/utils/media-manager.js @@ -0,0 +1,833 @@ +// 媒体文件管理器 - 微信小程序专用 +// 处理图片、视频、音频、文件的上传、下载、预览、缓存等 + +const apiClient = require('./api-client.js'); + +/** + * 媒体文件管理器 + * 功能: + * 1. 文件选择和上传 + * 2. 媒体文件预览 + * 3. 文件下载和缓存 + * 4. 文件压缩和优化 + * 5. 云存储管理 + * 6. 文件类型检测 + */ +class MediaManager { + constructor() { + this.isInitialized = false; + + // 媒体配置 + this.mediaConfig = { + // 图片配置 + image: { + maxSize: 10 * 1024 * 1024, // 10MB + maxCount: 9, // 最多选择9张 + quality: 80, // 压缩质量 + formats: ['jpg', 'jpeg', 'png', 'gif', 'webp'], + compressWidth: 1080 // 压缩宽度 + }, + + // 视频配置 + video: { + maxSize: 100 * 1024 * 1024, // 100MB + maxDuration: 300, // 最长5分钟 + formats: ['mp4', 'mov', 'avi'], + compressQuality: 'medium' + }, + + // 音频配置 + audio: { + maxSize: 20 * 1024 * 1024, // 20MB + maxDuration: 600, // 最长10分钟 + formats: ['mp3', 'wav', 'aac', 'm4a'], + sampleRate: 16000 + }, + + // 文件配置 + file: { + maxSize: 50 * 1024 * 1024, // 50MB + allowedTypes: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'zip', 'rar'], + maxCount: 5 + }, + + // 缓存配置 + cache: { + maxSize: 200 * 1024 * 1024, // 200MB + expireTime: 7 * 24 * 60 * 60 * 1000, // 7天 + cleanupInterval: 24 * 60 * 60 * 1000 // 24小时清理一次 + } + }; + + // 文件缓存 + this.fileCache = new Map(); + + // 上传队列 + this.uploadQueue = []; + + // 下载队列 + this.downloadQueue = []; + + // 当前上传任务 + this.currentUploads = new Map(); + + // 缓存统计 + this.cacheStats = { + totalSize: 0, + fileCount: 0, + lastCleanup: 0 + }; + + this.init(); + } + + // 初始化媒体管理器 + async init() { + if (this.isInitialized) return; + + console.log('📁 初始化媒体文件管理器...'); + + try { + // 加载文件缓存信息 + await this.loadCacheInfo(); + + // 检查存储权限 + await this.checkStoragePermission(); + + // 启动缓存清理 + this.startCacheCleanup(); + + this.isInitialized = true; + console.log('✅ 媒体文件管理器初始化完成'); + + } catch (error) { + console.error('❌ 媒体文件管理器初始化失败:', error); + } + } + + // 📷 ===== 图片处理 ===== + + // 选择图片 + async chooseImages(options = {}) { + try { + const { + count = this.mediaConfig.image.maxCount, + sizeType = ['compressed', 'original'], + sourceType = ['album', 'camera'] + } = options; + + console.log('📷 选择图片...'); + + const result = await new Promise((resolve, reject) => { + wx.chooseImage({ + count: count, + sizeType: sizeType, + sourceType: sourceType, + success: resolve, + fail: reject + }); + }); + + // 验证和处理图片 + const processedImages = await this.processImages(result.tempFilePaths); + + console.log(`📷 选择了 ${processedImages.length} 张图片`); + return { + success: true, + images: processedImages + }; + + } catch (error) { + console.error('❌ 选择图片失败:', error); + return { + success: false, + error: error.errMsg || '选择图片失败' + }; + } + } + + // 处理图片 + async processImages(tempFilePaths) { + const processedImages = []; + + for (const tempPath of tempFilePaths) { + try { + // 获取图片信息 + const imageInfo = await this.getImageInfo(tempPath); + + // 检查文件大小 + if (imageInfo.size > this.mediaConfig.image.maxSize) { + console.warn('⚠️ 图片过大,需要压缩:', imageInfo.size); + // 压缩图片 + const compressedPath = await this.compressImage(tempPath); + imageInfo.tempFilePath = compressedPath; + } + + // 生成缩略图 + const thumbnailPath = await this.generateThumbnail(imageInfo.tempFilePath); + + processedImages.push({ + ...imageInfo, + thumbnailPath: thumbnailPath, + type: 'image', + status: 'ready' + }); + + } catch (error) { + console.error('❌ 处理图片失败:', error); + } + } + + return processedImages; + } + + // 获取图片信息 + async getImageInfo(src) { + return new Promise((resolve, reject) => { + wx.getImageInfo({ + src: src, + success: (res) => { + // 获取文件大小 + wx.getFileInfo({ + filePath: src, + success: (fileInfo) => { + resolve({ + ...res, + size: fileInfo.size, + tempFilePath: src + }); + }, + fail: () => { + resolve({ + ...res, + size: 0, + tempFilePath: src + }); + } + }); + }, + fail: reject + }); + }); + } + + // 压缩图片 + async compressImage(src) { + try { + const result = await new Promise((resolve, reject) => { + wx.compressImage({ + src: src, + quality: this.mediaConfig.image.quality, + success: resolve, + fail: reject + }); + }); + + console.log('📷 图片压缩完成'); + return result.tempFilePath; + + } catch (error) { + console.error('❌ 图片压缩失败:', error); + return src; // 压缩失败返回原图 + } + } + + // 生成缩略图 + async generateThumbnail(src) { + try { + // 创建canvas生成缩略图 + const canvas = wx.createOffscreenCanvas({ type: '2d' }); + const ctx = canvas.getContext('2d'); + + // 设置缩略图尺寸 + const thumbnailSize = 200; + canvas.width = thumbnailSize; + canvas.height = thumbnailSize; + + // 加载图片 + const image = canvas.createImage(); + + return new Promise((resolve) => { + image.onload = () => { + // 计算绘制尺寸 + const { drawWidth, drawHeight, drawX, drawY } = this.calculateDrawSize( + image.width, + image.height, + thumbnailSize, + thumbnailSize + ); + + // 绘制缩略图 + ctx.drawImage(image, drawX, drawY, drawWidth, drawHeight); + + // 导出为临时文件 + wx.canvasToTempFilePath({ + canvas: canvas, + success: (res) => { + resolve(res.tempFilePath); + }, + fail: () => { + resolve(src); // 生成失败返回原图 + } + }); + }; + + image.onerror = () => { + resolve(src); // 加载失败返回原图 + }; + + image.src = src; + }); + + } catch (error) { + console.error('❌ 生成缩略图失败:', error); + return src; + } + } + + // 计算绘制尺寸 + calculateDrawSize(imageWidth, imageHeight, canvasWidth, canvasHeight) { + const imageRatio = imageWidth / imageHeight; + const canvasRatio = canvasWidth / canvasHeight; + + let drawWidth, drawHeight, drawX, drawY; + + if (imageRatio > canvasRatio) { + // 图片更宽,以高度为准 + drawHeight = canvasHeight; + drawWidth = drawHeight * imageRatio; + drawX = (canvasWidth - drawWidth) / 2; + drawY = 0; + } else { + // 图片更高,以宽度为准 + drawWidth = canvasWidth; + drawHeight = drawWidth / imageRatio; + drawX = 0; + drawY = (canvasHeight - drawHeight) / 2; + } + + return { drawWidth, drawHeight, drawX, drawY }; + } + + // 🎬 ===== 视频处理 ===== + + // 选择视频 + async chooseVideo(options = {}) { + try { + const { + sourceType = ['album', 'camera'], + maxDuration = this.mediaConfig.video.maxDuration, + camera = 'back' + } = options; + + console.log('🎬 选择视频...'); + + const result = await new Promise((resolve, reject) => { + wx.chooseVideo({ + sourceType: sourceType, + maxDuration: maxDuration, + camera: camera, + success: resolve, + fail: reject + }); + }); + + // 验证和处理视频 + const processedVideo = await this.processVideo(result); + + console.log('🎬 选择视频完成'); + return { + success: true, + video: processedVideo + }; + + } catch (error) { + console.error('❌ 选择视频失败:', error); + return { + success: false, + error: error.errMsg || '选择视频失败' + }; + } + } + + // 处理视频 + async processVideo(videoResult) { + try { + // 检查文件大小 + if (videoResult.size > this.mediaConfig.video.maxSize) { + throw new Error('视频文件过大'); + } + + // 检查时长 + if (videoResult.duration > this.mediaConfig.video.maxDuration) { + throw new Error('视频时长超出限制'); + } + + // 生成视频缩略图 + const thumbnailPath = await this.generateVideoThumbnail(videoResult.tempFilePath); + + return { + tempFilePath: videoResult.tempFilePath, + duration: videoResult.duration, + size: videoResult.size, + width: videoResult.width, + height: videoResult.height, + thumbnailPath: thumbnailPath, + type: 'video', + status: 'ready' + }; + + } catch (error) { + console.error('❌ 处理视频失败:', error); + throw error; + } + } + + // 生成视频缩略图 + async generateVideoThumbnail(videoPath) { + try { + // 微信小程序暂不支持视频帧提取,使用默认图标 + return null; + } catch (error) { + console.error('❌ 生成视频缩略图失败:', error); + return null; + } + } + + // 📄 ===== 文件处理 ===== + + // 选择文件 + async chooseFile(options = {}) { + try { + const { + count = this.mediaConfig.file.maxCount, + type = 'all' + } = options; + + console.log('📄 选择文件...'); + + const result = await new Promise((resolve, reject) => { + wx.chooseMessageFile({ + count: count, + type: type, + success: resolve, + fail: reject + }); + }); + + // 验证和处理文件 + const processedFiles = await this.processFiles(result.tempFiles); + + console.log(`📄 选择了 ${processedFiles.length} 个文件`); + return { + success: true, + files: processedFiles + }; + + } catch (error) { + console.error('❌ 选择文件失败:', error); + return { + success: false, + error: error.errMsg || '选择文件失败' + }; + } + } + + // 处理文件 + async processFiles(tempFiles) { + const processedFiles = []; + + for (const file of tempFiles) { + try { + // 检查文件大小 + if (file.size > this.mediaConfig.file.maxSize) { + console.warn('⚠️ 文件过大:', file.name, file.size); + continue; + } + + // 检查文件类型 + const fileExtension = this.getFileExtension(file.name); + if (!this.isAllowedFileType(fileExtension)) { + console.warn('⚠️ 不支持的文件类型:', fileExtension); + continue; + } + + processedFiles.push({ + tempFilePath: file.path, + name: file.name, + size: file.size, + type: 'file', + extension: fileExtension, + status: 'ready' + }); + + } catch (error) { + console.error('❌ 处理文件失败:', error); + } + } + + return processedFiles; + } + + // 获取文件扩展名 + getFileExtension(fileName) { + const lastDotIndex = fileName.lastIndexOf('.'); + if (lastDotIndex === -1) return ''; + return fileName.substring(lastDotIndex + 1).toLowerCase(); + } + + // 检查文件类型是否允许 + isAllowedFileType(extension) { + return this.mediaConfig.file.allowedTypes.includes(extension); + } + + // 📤 ===== 文件上传 ===== + + // 上传文件 + async uploadFile(file, options = {}) { + try { + const { + onProgress, + onSuccess, + onError + } = options; + + console.log('📤 开始上传文件:', file.name || 'unknown'); + + // 生成上传ID + const uploadId = this.generateUploadId(); + + // 添加到上传队列 + const uploadTask = { + id: uploadId, + file: file, + status: 'uploading', + progress: 0, + onProgress: onProgress, + onSuccess: onSuccess, + onError: onError + }; + + this.currentUploads.set(uploadId, uploadTask); + + // 执行上传 + const result = await this.performUpload(uploadTask); + + // 移除上传任务 + this.currentUploads.delete(uploadId); + + return result; + + } catch (error) { + console.error('❌ 上传文件失败:', error); + return { + success: false, + error: error.message + }; + } + } + + // 执行上传 + async performUpload(uploadTask) { + return new Promise((resolve, reject) => { + const uploadTask_wx = wx.uploadFile({ + url: `${apiClient.baseURL}/api/v1/files/upload`, + filePath: uploadTask.file.tempFilePath, + name: 'file', + header: { + 'Authorization': `Bearer ${wx.getStorageSync('token')}` + }, + formData: { + type: uploadTask.file.type, + name: uploadTask.file.name || 'unknown' + }, + success: (res) => { + try { + const data = JSON.parse(res.data); + if (data.success) { + console.log('✅ 文件上传成功:', data.data.url); + + const result = { + success: true, + data: { + url: data.data.url, + fileId: data.data.fileId, + fileName: uploadTask.file.name, + fileSize: uploadTask.file.size, + fileType: uploadTask.file.type + } + }; + + if (uploadTask.onSuccess) { + uploadTask.onSuccess(result); + } + + resolve(result); + } else { + throw new Error(data.error || '上传失败'); + } + } catch (error) { + reject(error); + } + }, + fail: (error) => { + console.error('❌ 上传请求失败:', error); + if (uploadTask.onError) { + uploadTask.onError(error); + } + reject(error); + } + }); + + // 监听上传进度 + uploadTask_wx.onProgressUpdate((res) => { + uploadTask.progress = res.progress; + + if (uploadTask.onProgress) { + uploadTask.onProgress({ + progress: res.progress, + totalBytesSent: res.totalBytesSent, + totalBytesExpectedToSend: res.totalBytesExpectedToSend + }); + } + }); + + // 保存上传任务引用 + uploadTask.wxTask = uploadTask_wx; + }); + } + + // 取消上传 + cancelUpload(uploadId) { + const uploadTask = this.currentUploads.get(uploadId); + if (uploadTask && uploadTask.wxTask) { + uploadTask.wxTask.abort(); + this.currentUploads.delete(uploadId); + console.log('📤 取消上传:', uploadId); + } + } + + // 生成上传ID + generateUploadId() { + return `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + // 📥 ===== 文件下载和缓存 ===== + + // 下载文件 + async downloadFile(url, options = {}) { + try { + const { + fileName, + onProgress, + useCache = true + } = options; + + console.log('📥 下载文件:', url); + + // 检查缓存 + if (useCache) { + const cachedPath = this.getCachedFilePath(url); + if (cachedPath) { + console.log('📥 使用缓存文件:', cachedPath); + return { + success: true, + tempFilePath: cachedPath, + cached: true + }; + } + } + + // 执行下载 + const result = await this.performDownload(url, { fileName, onProgress }); + + // 缓存文件 + if (result.success && useCache) { + this.cacheFile(url, result.tempFilePath); + } + + return result; + + } catch (error) { + console.error('❌ 下载文件失败:', error); + return { + success: false, + error: error.message + }; + } + } + + // 执行下载 + async performDownload(url, options = {}) { + return new Promise((resolve, reject) => { + const downloadTask = wx.downloadFile({ + url: url, + success: (res) => { + if (res.statusCode === 200) { + console.log('✅ 文件下载成功'); + resolve({ + success: true, + tempFilePath: res.tempFilePath, + cached: false + }); + } else { + reject(new Error(`下载失败: ${res.statusCode}`)); + } + }, + fail: reject + }); + + // 监听下载进度 + if (options.onProgress) { + downloadTask.onProgressUpdate((res) => { + options.onProgress({ + progress: res.progress, + totalBytesWritten: res.totalBytesWritten, + totalBytesExpectedToWrite: res.totalBytesExpectedToWrite + }); + }); + } + }); + } + + // 检查存储权限 + async checkStoragePermission() { + try { + const storageInfo = wx.getStorageInfoSync(); + console.log('📁 存储信息:', storageInfo); + return true; + } catch (error) { + console.error('❌ 检查存储权限失败:', error); + return false; + } + } + + // 加载缓存信息 + async loadCacheInfo() { + try { + const cacheInfo = wx.getStorageSync('mediaCacheInfo') || {}; + this.cacheStats = { + totalSize: cacheInfo.totalSize || 0, + fileCount: cacheInfo.fileCount || 0, + lastCleanup: cacheInfo.lastCleanup || 0 + }; + } catch (error) { + console.error('❌ 加载缓存信息失败:', error); + } + } + + // 保存缓存信息 + async saveCacheInfo() { + try { + wx.setStorageSync('mediaCacheInfo', this.cacheStats); + } catch (error) { + console.error('❌ 保存缓存信息失败:', error); + } + } + + // 获取缓存文件路径 + getCachedFilePath(url) { + const cacheKey = this.generateCacheKey(url); + return this.fileCache.get(cacheKey); + } + + // 缓存文件 + cacheFile(url, filePath) { + const cacheKey = this.generateCacheKey(url); + this.fileCache.set(cacheKey, filePath); + + // 更新缓存统计 + this.cacheStats.fileCount++; + this.saveCacheInfo(); + } + + // 生成缓存键 + generateCacheKey(url) { + // 使用URL的hash作为缓存键 + let hash = 0; + for (let i = 0; i < url.length; i++) { + const char = url.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // 转换为32位整数 + } + return `cache_${Math.abs(hash)}`; + } + + // 启动缓存清理 + startCacheCleanup() { + setInterval(() => { + this.performCacheCleanup(); + }, this.mediaConfig.cache.cleanupInterval); + } + + // 执行缓存清理 + performCacheCleanup() { + try { + console.log('📁 执行缓存清理...'); + + const now = Date.now(); + const expireTime = this.mediaConfig.cache.expireTime; + + // 清理过期缓存 + for (const [key, filePath] of this.fileCache) { + try { + const stats = wx.getFileInfo({ filePath }); + if (now - stats.createTime > expireTime) { + this.fileCache.delete(key); + // 删除文件 + wx.removeSavedFile({ filePath }); + } + } catch (error) { + // 文件不存在,从缓存中移除 + this.fileCache.delete(key); + } + } + + // 更新清理时间 + this.cacheStats.lastCleanup = now; + this.saveCacheInfo(); + + console.log('✅ 缓存清理完成'); + + } catch (error) { + console.error('❌ 缓存清理失败:', error); + } + } + + // 获取媒体管理器状态 + getStatus() { + return { + isInitialized: this.isInitialized, + uploadCount: this.currentUploads.size, + cacheStats: { ...this.cacheStats }, + config: this.mediaConfig + }; + } + + // 清除所有缓存 + clearAllCache() { + this.fileCache.clear(); + this.cacheStats = { + totalSize: 0, + fileCount: 0, + lastCleanup: Date.now() + }; + this.saveCacheInfo(); + console.log('📁 已清除所有媒体缓存'); + } + + // 重置管理器 + reset() { + // 取消所有上传任务 + for (const [uploadId] of this.currentUploads) { + this.cancelUpload(uploadId); + } + + this.clearAllCache(); + } +} + +// 创建全局实例 +const mediaManager = new MediaManager(); + +module.exports = mediaManager; diff --git a/utils/media-picker.js b/utils/media-picker.js new file mode 100644 index 0000000..0642f0e --- /dev/null +++ b/utils/media-picker.js @@ -0,0 +1,366 @@ +// 媒体选择工具类 - 支持拍照、拍视频、相册选择 +class MediaPicker { + constructor() { + this.maxImageCount = 9; // 最多选择9张图片 + this.maxVideoCount = 1; // 最多选择1个视频 + this.maxVideoDuration = 60; // 视频最长60秒 + this.maxImageSize = 10 * 1024 * 1024; // 图片最大10MB + this.maxVideoSize = 100 * 1024 * 1024; // 视频最大100MB + } + + // 🔥 ===== 图片选择功能 ===== + + /** + * 选择图片 - 支持拍照和相册 + * @param {Object} options 选项 + * @param {number} options.count 最多选择数量,默认9 + * @param {Array} options.sourceType 来源类型 ['album', 'camera'] + * @param {Array} options.sizeType 图片尺寸 ['original', 'compressed'] + * @returns {Promise} 返回选择的图片信息 + */ + async chooseImages(options = {}) { + const { + count = this.maxImageCount, + sourceType = ['album', 'camera'], + sizeType = ['compressed', 'original'] + } = options; + + console.log('📸 开始选择图片:', { count, sourceType, sizeType }); + + try { + // 优先使用新版API + if (wx.chooseMedia) { + return await this.chooseMediaImages({ count, sourceType, sizeType }); + } else { + return await this.chooseImageLegacy({ count, sourceType, sizeType }); + } + } catch (error) { + console.error('❌ 选择图片失败:', error); + throw error; + } + } + + /** + * 使用新版chooseMedia API选择图片 + */ + chooseMediaImages(options) { + return new Promise((resolve, reject) => { + wx.chooseMedia({ + count: options.count, + mediaType: ['image'], + sourceType: options.sourceType, + camera: 'back', + success: (res) => { + console.log('✅ chooseMedia选择图片成功:', res); + + const images = res.tempFiles.map(file => ({ + path: file.tempFilePath, + size: file.size, + type: 'image', + width: file.width || 0, + height: file.height || 0, + duration: 0 + })); + + resolve({ + success: true, + files: images, + count: images.length + }); + }, + fail: (error) => { + console.error('❌ chooseMedia选择图片失败:', error); + reject(error); + } + }); + }); + } + + /** + * 使用传统chooseImage API选择图片 + */ + chooseImageLegacy(options) { + return new Promise((resolve, reject) => { + wx.chooseImage({ + count: options.count, + sizeType: options.sizeType, + sourceType: options.sourceType, + success: (res) => { + console.log('✅ chooseImage选择图片成功:', res); + + const images = res.tempFilePaths.map((path, index) => ({ + path: path, + size: res.tempFiles ? res.tempFiles[index]?.size || 0 : 0, + type: 'image', + width: 0, + height: 0, + duration: 0 + })); + + resolve({ + success: true, + files: images, + count: images.length + }); + }, + fail: (error) => { + console.error('❌ chooseImage选择图片失败:', error); + reject(error); + } + }); + }); + } + + // 🔥 ===== 视频选择功能 ===== + + /** + * 选择视频 - 支持拍摄和相册 + * @param {Object} options 选项 + * @param {Array} options.sourceType 来源类型 ['album', 'camera'] + * @param {number} options.maxDuration 最大时长(秒) + * @param {string} options.camera 摄像头 'front'|'back' + * @returns {Promise} 返回选择的视频信息 + */ + async chooseVideo(options = {}) { + const { + sourceType = ['album', 'camera'], + maxDuration = this.maxVideoDuration, + camera = 'back' + } = options; + + console.log('🎥 开始选择视频:', { sourceType, maxDuration, camera }); + + try { + // 优先使用新版API + if (wx.chooseMedia) { + return await this.chooseMediaVideo({ sourceType, maxDuration, camera }); + } else { + return await this.chooseVideoLegacy({ sourceType, maxDuration, camera }); + } + } catch (error) { + console.error('❌ 选择视频失败:', error); + throw error; + } + } + + /** + * 使用新版chooseMedia API选择视频 + */ + chooseMediaVideo(options) { + return new Promise((resolve, reject) => { + wx.chooseMedia({ + count: 1, + mediaType: ['video'], + sourceType: options.sourceType, + maxDuration: options.maxDuration, + camera: options.camera, + success: (res) => { + console.log('✅ chooseMedia选择视频成功:', res); + // 健壮性判断,防止tempFiles为空或无元素 + if (!res.tempFiles || !Array.isArray(res.tempFiles) || res.tempFiles.length === 0) { + console.warn('⚠️ chooseMedia返回空tempFiles:', res); + resolve({ success: false, files: [], count: 0, message: '未选择视频' }); + return; + } + const video = res.tempFiles[0]; + if (!video || !video.tempFilePath) { + console.warn('⚠️ chooseMedia返回的video对象异常:', video); + resolve({ success: false, files: [], count: 0, message: '未选择有效视频' }); + return; + } + const videoInfo = { + path: video.tempFilePath, + size: video.size, + type: 'video', + width: video.width || 0, + height: video.height || 0, + duration: video.duration || 0, + thumbTempFilePath: video.thumbTempFilePath || '' + }; + resolve({ + success: true, + files: [videoInfo], + count: 1 + }); + }, + fail: (error) => { + console.error('❌ chooseMedia选择视频失败:', error); + reject(error); + } + }); + }); + } + + /** + * 使用传统chooseVideo API选择视频 + */ + chooseVideoLegacy(options) { + return new Promise((resolve, reject) => { + wx.chooseVideo({ + sourceType: options.sourceType, + maxDuration: options.maxDuration, + camera: options.camera, + success: (res) => { + console.log('✅ chooseVideo选择视频成功:', res); + + const videoInfo = { + path: res.tempFilePath, + size: res.size || 0, + type: 'video', + width: res.width || 0, + height: res.height || 0, + duration: res.duration || 0, + thumbTempFilePath: res.thumbTempFilePath || '' + }; + + resolve({ + success: true, + files: [videoInfo], + count: 1 + }); + }, + fail: (error) => { + console.error('❌ chooseVideo选择视频失败:', error); + reject(error); + } + }); + }); + } + + // 🔥 ===== 混合媒体选择功能 ===== + + /** + * 选择混合媒体 - 图片和视频 + * @param {Object} options 选项 + * @returns {Promise} 返回选择的媒体信息 + */ + async chooseMedia(options = {}) { + const { + count = 9, + mediaType = ['image', 'video'], + sourceType = ['album', 'camera'], + maxDuration = this.maxVideoDuration, + camera = 'back' + } = options; + + console.log('📱 开始选择混合媒体:', { count, mediaType, sourceType }); + + if (!wx.chooseMedia) { + throw new Error('当前微信版本不支持chooseMedia API'); + } + + return new Promise((resolve, reject) => { + wx.chooseMedia({ + count: count, + mediaType: mediaType, + sourceType: sourceType, + maxDuration: maxDuration, + camera: camera, + success: (res) => { + console.log('✅ chooseMedia选择媒体成功:', res); + + const files = res.tempFiles.map(file => ({ + path: file.tempFilePath, + size: file.size, + type: file.fileType, // 'image' 或 'video' + width: file.width || 0, + height: file.height || 0, + duration: file.duration || 0, + thumbTempFilePath: file.thumbTempFilePath || '' + })); + + resolve({ + success: true, + files: files, + count: files.length + }); + }, + fail: (error) => { + console.error('❌ chooseMedia选择媒体失败:', error); + reject(error); + } + }); + }); + } + + // 🔥 ===== 工具方法 ===== + + /** + * 检查文件大小 + */ + checkFileSize(file) { + const maxSize = file.type === 'image' ? this.maxImageSize : this.maxVideoSize; + if (file.size > maxSize) { + const maxSizeMB = Math.round(maxSize / 1024 / 1024); + throw new Error(`文件大小超过限制,最大支持${maxSizeMB}MB`); + } + return true; + } + + /** + * 获取文件信息 + */ + async getFileInfo(filePath) { + return new Promise((resolve, reject) => { + wx.getFileInfo({ + filePath: filePath, + success: resolve, + fail: reject + }); + }); + } + + /** + * 显示选择媒体的操作菜单 + */ + showMediaActionSheet(options = {}) { + const { + showCamera = true, + showAlbum = true, + showVideo = true + } = options; + + const itemList = []; + const actions = []; + + if (showCamera) { + itemList.push('拍照'); + actions.push(() => this.chooseImages({ sourceType: ['camera'] })); + } + + if (showAlbum) { + itemList.push('从相册选择图片'); + actions.push(() => this.chooseImages({ sourceType: ['album'] })); + } + + if (showVideo) { + itemList.push('拍摄视频'); + actions.push(() => this.chooseVideo({ sourceType: ['camera'] })); + + itemList.push('从相册选择视频'); + actions.push(() => this.chooseVideo({ sourceType: ['album'] })); + } + + return new Promise((resolve, reject) => { + wx.showActionSheet({ + itemList: itemList, + success: (res) => { + const selectedAction = actions[res.tapIndex]; + if (selectedAction) { + selectedAction().then(resolve).catch(reject); + } + }, + fail: (error) => { + if (error.errMsg !== 'showActionSheet:fail cancel') { + reject(error); + } + } + }); + }); + } +} + +// 创建全局单例 +const mediaPicker = new MediaPicker(); + +module.exports = mediaPicker; diff --git a/utils/message-history-manager.js b/utils/message-history-manager.js new file mode 100644 index 0000000..8958ce5 --- /dev/null +++ b/utils/message-history-manager.js @@ -0,0 +1,671 @@ +// 消息历史记录管理器 - 微信小程序专用 +// 处理消息历史记录的本地缓存、分页加载、存储优化等 + +const apiClient = require('./api-client.js'); + +/** + * 消息历史记录管理器 + * 功能: + * 1. 消息历史记录缓存 + * 2. 分页加载优化 + * 3. 存储空间管理 + * 4. 消息去重和排序 + * 5. 离线消息处理 + * 6. 数据压缩存储 + */ +class MessageHistoryManager { + constructor() { + this.isInitialized = false; + + // 历史记录配置 + this.historyConfig = { + // 每页消息数量 + pageSize: 20, + + // 本地缓存的最大消息数量(每个会话) + maxCachedMessages: 500, + + // 缓存过期时间(毫秒) + cacheExpireTime: 24 * 60 * 60 * 1000, // 24小时 + + // 存储压缩阈值(字节) + compressionThreshold: 1024, + + // 自动清理间隔(毫秒) + cleanupInterval: 60 * 60 * 1000, // 1小时 + + // 预加载页数 + preloadPages: 2 + }; + + // 消息缓存 Map + this.messageCache = new Map(); + + // 加载状态 Map + this.loadingStates = new Map(); + + // 存储统计 + this.storageStats = { + totalSize: 0, + messageCount: 0, + conversationCount: 0, + lastCleanup: 0 + }; + + // 清理定时器 + this.cleanupTimer = null; + + this.init(); + } + + // 初始化历史记录管理器 + async init() { + if (this.isInitialized) return; + + console.log('📚 初始化消息历史记录管理器...'); + + try { + // 加载缓存的消息 + await this.loadCachedMessages(); + + // 计算存储统计 + this.calculateStorageStats(); + + // 启动定时清理 + this.startCleanupTimer(); + + this.isInitialized = true; + console.log('✅ 消息历史记录管理器初始化完成'); + + } catch (error) { + console.error('❌ 消息历史记录管理器初始化失败:', error); + } + } + + // 获取会话消息历史 + async getConversationHistory(conversationId, options = {}) { + try { + console.log('📚 获取会话消息历史:', conversationId); + + const { + page = 1, + pageSize = this.historyConfig.pageSize, + forceRefresh = false, + loadDirection = 'up' // 'up' 向上加载更早的消息, 'down' 向下加载更新的消息 + } = options; + + // 获取或创建消息缓存 + let messageCache = this.getMessageCache(conversationId); + if (!messageCache) { + messageCache = this.createMessageCache(conversationId); + } + + // 检查是否需要从服务器加载 + const needServerLoad = forceRefresh || + this.shouldLoadFromServer(messageCache, page, pageSize, loadDirection); + + if (needServerLoad) { + // 从服务器加载消息 + const serverResult = await this.loadMessagesFromServer( + conversationId, + page, + pageSize, + loadDirection, + messageCache + ); + + if (!serverResult.success) { + return serverResult; + } + } + + // 从缓存获取消息 + const messages = this.getMessagesFromCache(messageCache, page, pageSize, loadDirection); + + return { + success: true, + data: { + messages: messages, + page: page, + pageSize: pageSize, + total: messageCache.totalCount, + hasMore: this.hasMoreMessages(messageCache, page, pageSize, loadDirection), + cached: !needServerLoad + } + }; + + } catch (error) { + console.error('❌ 获取会话消息历史失败:', error); + return { success: false, error: error.message }; + } + } + + // 预加载消息 + async preloadMessages(conversationId, currentPage = 1) { + try { + console.log('📚 预加载消息:', conversationId); + + const preloadPromises = []; + + // 预加载后续页面 + for (let i = 1; i <= this.historyConfig.preloadPages; i++) { + const nextPage = currentPage + i; + preloadPromises.push( + this.getConversationHistory(conversationId, { + page: nextPage, + loadDirection: 'up' + }) + ); + } + + // 并行执行预加载 + await Promise.allSettled(preloadPromises); + console.log('✅ 消息预加载完成'); + + } catch (error) { + console.error('❌ 消息预加载失败:', error); + } + } + + // 添加新消息到缓存 + addMessageToCache(conversationId, message) { + try { + let messageCache = this.getMessageCache(conversationId); + if (!messageCache) { + messageCache = this.createMessageCache(conversationId); + } + + // 检查消息是否已存在 + const existingIndex = messageCache.messages.findIndex(m => m.id === message.id); + if (existingIndex !== -1) { + // 更新现有消息 + messageCache.messages[existingIndex] = message; + } else { + // 添加新消息(按时间排序) + const insertIndex = this.findInsertIndex(messageCache.messages, message); + messageCache.messages.splice(insertIndex, 0, message); + messageCache.totalCount++; + } + + // 限制缓存大小 + this.limitCacheSize(messageCache); + + // 更新缓存时间 + messageCache.lastUpdated = Date.now(); + + // 保存到本地存储 + this.saveMessageCache(conversationId, messageCache); + + console.log('📚 消息已添加到缓存:', message.id); + + } catch (error) { + console.error('❌ 添加消息到缓存失败:', error); + } + } + + // 更新消息状态 + updateMessageInCache(conversationId, messageId, updates) { + try { + const messageCache = this.getMessageCache(conversationId); + if (!messageCache) return false; + + const messageIndex = messageCache.messages.findIndex(m => m.id === messageId); + if (messageIndex === -1) return false; + + // 更新消息 + messageCache.messages[messageIndex] = { + ...messageCache.messages[messageIndex], + ...updates + }; + + // 更新缓存时间 + messageCache.lastUpdated = Date.now(); + + // 保存到本地存储 + this.saveMessageCache(conversationId, messageCache); + + console.log('📚 消息状态已更新:', messageId); + return true; + + } catch (error) { + console.error('❌ 更新消息状态失败:', error); + return false; + } + } + + // 删除消息 + deleteMessageFromCache(conversationId, messageId) { + try { + const messageCache = this.getMessageCache(conversationId); + if (!messageCache) return false; + + const messageIndex = messageCache.messages.findIndex(m => m.id === messageId); + if (messageIndex === -1) return false; + + // 删除消息 + messageCache.messages.splice(messageIndex, 1); + messageCache.totalCount--; + + // 更新缓存时间 + messageCache.lastUpdated = Date.now(); + + // 保存到本地存储 + this.saveMessageCache(conversationId, messageCache); + + console.log('📚 消息已从缓存删除:', messageId); + return true; + + } catch (error) { + console.error('❌ 删除消息失败:', error); + return false; + } + } + + // 从服务器加载消息 + async loadMessagesFromServer(conversationId, page, pageSize, loadDirection, messageCache) { + try { + // 设置加载状态 + this.setLoadingState(conversationId, true); + + // 计算请求参数 + const requestParams = this.calculateRequestParams( + messageCache, + page, + pageSize, + loadDirection + ); + + // 调用API + const response = await apiClient.request({ + url: '/api/v1/messages/history', + method: 'GET', + data: { + conversationId: conversationId, + ...requestParams + } + }); + + if (response.success) { + // 处理返回的消息 + const messages = response.data.messages || []; + const total = response.data.total || 0; + + // 更新缓存 + this.updateCacheWithServerData(messageCache, messages, total, loadDirection); + + // 保存到本地存储 + this.saveMessageCache(conversationId, messageCache); + + console.log(`📚 从服务器加载了 ${messages.length} 条消息`); + return { success: true }; + + } else { + throw new Error(response.error || '加载消息失败'); + } + + } catch (error) { + console.error('❌ 从服务器加载消息失败:', error); + return { success: false, error: error.message }; + + } finally { + this.setLoadingState(conversationId, false); + } + } + + // 计算请求参数 + calculateRequestParams(messageCache, page, pageSize, loadDirection) { + const params = { + pageSize: pageSize + }; + + if (loadDirection === 'up') { + // 向上加载更早的消息 + if (messageCache.messages.length > 0) { + const oldestMessage = messageCache.messages[messageCache.messages.length - 1]; + params.beforeTimestamp = oldestMessage.timestamp; + } + } else { + // 向下加载更新的消息 + if (messageCache.messages.length > 0) { + const newestMessage = messageCache.messages[0]; + params.afterTimestamp = newestMessage.timestamp; + } + } + + return params; + } + + // 更新缓存数据 + updateCacheWithServerData(messageCache, newMessages, total, loadDirection) { + if (loadDirection === 'up') { + // 向上加载:添加到数组末尾(更早的消息) + messageCache.messages.push(...newMessages); + } else { + // 向下加载:添加到数组开头(更新的消息) + messageCache.messages.unshift(...newMessages); + } + + // 去重和排序 + this.deduplicateAndSortMessages(messageCache); + + // 更新总数 + messageCache.totalCount = Math.max(total, messageCache.messages.length); + + // 限制缓存大小 + this.limitCacheSize(messageCache); + + // 更新时间戳 + messageCache.lastUpdated = Date.now(); + } + + // 消息去重和排序 + deduplicateAndSortMessages(messageCache) { + // 去重 + const uniqueMessages = new Map(); + messageCache.messages.forEach(message => { + uniqueMessages.set(message.id, message); + }); + + // 排序(最新的在前面) + messageCache.messages = Array.from(uniqueMessages.values()) + .sort((a, b) => b.timestamp - a.timestamp); + } + + // 查找插入位置 + findInsertIndex(messages, newMessage) { + for (let i = 0; i < messages.length; i++) { + if (newMessage.timestamp > messages[i].timestamp) { + return i; + } + } + return messages.length; + } + + // 限制缓存大小 + limitCacheSize(messageCache) { + if (messageCache.messages.length > this.historyConfig.maxCachedMessages) { + // 保留最新的消息 + const keepCount = Math.floor(this.historyConfig.maxCachedMessages * 0.8); + messageCache.messages = messageCache.messages.slice(0, keepCount); + console.log(`📚 缓存大小已限制到 ${keepCount} 条消息`); + } + } + + // 获取消息缓存 + getMessageCache(conversationId) { + return this.messageCache.get(conversationId); + } + + // 创建消息缓存 + createMessageCache(conversationId) { + const cache = { + conversationId: conversationId, + messages: [], + totalCount: 0, + lastUpdated: Date.now(), + loadedPages: new Set(), + hasMoreUp: true, + hasMoreDown: false + }; + + this.messageCache.set(conversationId, cache); + return cache; + } + + // 从缓存获取消息 + getMessagesFromCache(messageCache, page, pageSize, loadDirection) { + const startIndex = (page - 1) * pageSize; + const endIndex = startIndex + pageSize; + + return messageCache.messages.slice(startIndex, endIndex); + } + + // 检查是否有更多消息 + hasMoreMessages(messageCache, page, pageSize, loadDirection) { + if (loadDirection === 'up') { + return messageCache.hasMoreUp && + (page * pageSize) < messageCache.totalCount; + } else { + return messageCache.hasMoreDown; + } + } + + // 检查是否需要从服务器加载 + shouldLoadFromServer(messageCache, page, pageSize, loadDirection) { + // 如果缓存为空,需要加载 + if (messageCache.messages.length === 0) { + return true; + } + + // 如果请求的页面超出缓存范围,需要加载 + const startIndex = (page - 1) * pageSize; + const endIndex = startIndex + pageSize; + + if (endIndex > messageCache.messages.length && messageCache.hasMoreUp) { + return true; + } + + // 如果缓存过期,需要刷新 + const cacheAge = Date.now() - messageCache.lastUpdated; + if (cacheAge > this.historyConfig.cacheExpireTime) { + return true; + } + + return false; + } + + // 设置加载状态 + setLoadingState(conversationId, loading) { + this.loadingStates.set(conversationId, { + loading: loading, + timestamp: Date.now() + }); + } + + // 获取加载状态 + getLoadingState(conversationId) { + const state = this.loadingStates.get(conversationId); + return state ? state.loading : false; + } + + // 保存消息缓存到本地存储 + saveMessageCache(conversationId, messageCache) { + try { + const cacheKey = `message_cache_${conversationId}`; + const cacheData = this.compressMessageCache(messageCache); + + wx.setStorageSync(cacheKey, cacheData); + + } catch (error) { + console.error('❌ 保存消息缓存失败:', error); + } + } + + // 加载缓存的消息 + async loadCachedMessages() { + try { + const storageInfo = wx.getStorageInfoSync(); + const cacheKeys = storageInfo.keys.filter(key => key.startsWith('message_cache_')); + + for (const key of cacheKeys) { + try { + const conversationId = key.replace('message_cache_', ''); + const cacheData = wx.getStorageSync(key); + + if (cacheData) { + const messageCache = this.decompressMessageCache(cacheData); + this.messageCache.set(conversationId, messageCache); + } + + } catch (error) { + console.error(`❌ 加载缓存失败 [${key}]:`, error); + // 删除损坏的缓存 + wx.removeStorageSync(key); + } + } + + console.log(`📚 加载了 ${this.messageCache.size} 个会话的消息缓存`); + + } catch (error) { + console.error('❌ 加载缓存消息失败:', error); + } + } + + // 压缩消息缓存 + compressMessageCache(messageCache) { + try { + const data = JSON.stringify(messageCache); + + // 如果数据较大,可以考虑压缩 + if (data.length > this.historyConfig.compressionThreshold) { + // 这里可以实现压缩算法 + // 目前直接返回原数据 + return { compressed: false, data: messageCache }; + } + + return { compressed: false, data: messageCache }; + + } catch (error) { + console.error('❌ 压缩消息缓存失败:', error); + return { compressed: false, data: messageCache }; + } + } + + // 解压消息缓存 + decompressMessageCache(cacheData) { + try { + if (cacheData.compressed) { + // 这里可以实现解压算法 + return cacheData.data; + } + + return cacheData.data; + + } catch (error) { + console.error('❌ 解压消息缓存失败:', error); + return null; + } + } + + // 计算存储统计 + calculateStorageStats() { + let totalSize = 0; + let messageCount = 0; + + for (const [conversationId, cache] of this.messageCache) { + messageCount += cache.messages.length; + + // 估算缓存大小 + const cacheSize = JSON.stringify(cache).length; + totalSize += cacheSize; + } + + this.storageStats = { + totalSize: totalSize, + messageCount: messageCount, + conversationCount: this.messageCache.size, + lastCleanup: Date.now() + }; + } + + // 启动定时清理 + startCleanupTimer() { + if (this.cleanupTimer) { + clearInterval(this.cleanupTimer); + } + + this.cleanupTimer = setInterval(() => { + this.performCleanup(); + }, this.historyConfig.cleanupInterval); + } + + // 执行清理 + performCleanup() { + try { + console.log('📚 执行消息缓存清理...'); + + const now = Date.now(); + let cleanedCount = 0; + + for (const [conversationId, cache] of this.messageCache) { + // 清理过期缓存 + const cacheAge = now - cache.lastUpdated; + if (cacheAge > this.historyConfig.cacheExpireTime * 2) { + this.messageCache.delete(conversationId); + wx.removeStorageSync(`message_cache_${conversationId}`); + cleanedCount++; + } + } + + // 更新统计 + this.calculateStorageStats(); + + console.log(`📚 清理完成,删除了 ${cleanedCount} 个过期缓存`); + + } catch (error) { + console.error('❌ 消息缓存清理失败:', error); + } + } + + // 清除会话缓存 + clearConversationCache(conversationId) { + this.messageCache.delete(conversationId); + wx.removeStorageSync(`message_cache_${conversationId}`); + console.log('📚 已清除会话缓存:', conversationId); + } + + // 清除所有缓存 + clearAllCache() { + for (const conversationId of this.messageCache.keys()) { + wx.removeStorageSync(`message_cache_${conversationId}`); + } + this.messageCache.clear(); + this.calculateStorageStats(); + console.log('📚 已清除所有消息缓存'); + } + + // 获取存储统计 + getStorageStats() { + this.calculateStorageStats(); + return { ...this.storageStats }; + } + + // 获取会话统计 + getConversationStats(conversationId) { + const cache = this.getMessageCache(conversationId); + if (!cache) { + return null; + } + + return { + messageCount: cache.messages.length, + totalCount: cache.totalCount, + lastUpdated: cache.lastUpdated, + cacheAge: Date.now() - cache.lastUpdated, + hasMoreUp: cache.hasMoreUp, + hasMoreDown: cache.hasMoreDown + }; + } + + // 重置管理器 + reset() { + this.clearAllCache(); + this.loadingStates.clear(); + + if (this.cleanupTimer) { + clearInterval(this.cleanupTimer); + this.cleanupTimer = null; + } + } + + // 销毁管理器 + destroy() { + this.reset(); + this.isInitialized = false; + } +} + +// 创建全局实例 +const messageHistoryManager = new MessageHistoryManager(); + +module.exports = messageHistoryManager; diff --git a/utils/message-interaction-manager.js b/utils/message-interaction-manager.js new file mode 100644 index 0000000..ce5055d --- /dev/null +++ b/utils/message-interaction-manager.js @@ -0,0 +1,725 @@ +// 消息交互管理器 - 微信小程序专用 +// 处理消息表情回应、引用回复、撤回、转发等交互功能 + +const apiClient = require('./api-client.js'); + +/** + * 消息交互管理器 + * 功能: + * 1. 消息表情回应 + * 2. 消息引用回复 + * 3. 消息撤回功能 + * 4. 消息转发功能 + * 5. 消息收藏功能 + * 6. 消息多选操作 + */ +class MessageInteractionManager { + constructor() { + this.isInitialized = false; + + // 交互配置 + this.interactionConfig = { + // 表情回应配置 + reactions: { + enabled: true, + maxReactions: 6, // 每条消息最多6种表情 + maxUsersPerReaction: 100, // 每种表情最多100个用户 + commonEmojis: ['👍', '❤️', '😂', '😮', '😢', '😡'], // 常用表情 + customEmojis: [] // 自定义表情 + }, + + // 引用回复配置 + quote: { + enabled: true, + maxQuoteLength: 100, // 引用内容最大长度 + showOriginalSender: true, // 显示原发送者 + showTimestamp: true // 显示时间戳 + }, + + // 撤回配置 + recall: { + enabled: true, + timeLimit: 2 * 60 * 1000, // 2分钟内可撤回 + showRecallMessage: true, // 显示撤回提示 + allowRecallOthers: false // 是否允许撤回他人消息(群主/管理员) + }, + + // 转发配置 + forward: { + enabled: true, + maxForwardCount: 5, // 最多同时转发5条消息 + preserveFormat: true, // 保持原格式 + showForwardSource: true // 显示转发来源 + }, + + // 收藏配置 + favorite: { + enabled: true, + maxFavorites: 1000, // 最多收藏1000条 + syncToCloud: true // 同步到云端 + } + }; + + // 当前操作状态 + this.currentOperation = { + type: null, // 'reaction', 'quote', 'forward', 'select' + messageId: null, + data: null + }; + + // 多选状态 + this.multiSelectMode = false; + this.selectedMessages = new Set(); + + // 表情回应缓存 + this.reactionsCache = new Map(); + + this.init(); + } + + // 初始化交互管理器 + async init() { + if (this.isInitialized) return; + + console.log('✨ 初始化消息交互管理器...'); + + try { + // 加载用户收藏 + await this.loadUserFavorites(); + + // 加载表情回应缓存 + await this.loadReactionsCache(); + + this.isInitialized = true; + console.log('✅ 消息交互管理器初始化完成'); + + } catch (error) { + console.error('❌ 消息交互管理器初始化失败:', error); + } + } + + // 👍 ===== 表情回应功能 ===== + + // 添加表情回应 + async addReaction(messageId, emoji, userId) { + try { + console.log('👍 添加表情回应:', messageId, emoji, userId); + + // 验证参数 + if (!messageId || !emoji || !userId) { + throw new Error('参数不完整'); + } + + // 检查是否已经回应过 + const existingReaction = await this.getUserReaction(messageId, userId); + if (existingReaction === emoji) { + console.log('⚠️ 用户已经添加过相同表情'); + return { success: false, error: '已经添加过相同表情' }; + } + + // 如果用户已有其他表情回应,先移除 + if (existingReaction) { + await this.removeReaction(messageId, existingReaction, userId); + } + + // 调用API添加表情回应 + const response = await apiClient.request({ + url: '/api/v1/messages/reactions', + method: 'POST', + data: { + messageId: messageId, + emoji: emoji, + userId: userId + } + }); + + if (response.success) { + // 更新本地缓存 + this.updateReactionCache(messageId, emoji, userId, 'add'); + + console.log('✅ 表情回应添加成功'); + return { success: true, data: response.data }; + } else { + throw new Error(response.error || '添加表情回应失败'); + } + + } catch (error) { + console.error('❌ 添加表情回应失败:', error); + return { success: false, error: error.message }; + } + } + + // 移除表情回应 + async removeReaction(messageId, emoji, userId) { + try { + console.log('👎 移除表情回应:', messageId, emoji, userId); + + // 调用API移除表情回应 + const response = await apiClient.request({ + url: '/api/v1/messages/reactions', + method: 'DELETE', + data: { + messageId: messageId, + emoji: emoji, + userId: userId + } + }); + + if (response.success) { + // 更新本地缓存 + this.updateReactionCache(messageId, emoji, userId, 'remove'); + + console.log('✅ 表情回应移除成功'); + return { success: true }; + } else { + throw new Error(response.error || '移除表情回应失败'); + } + + } catch (error) { + console.error('❌ 移除表情回应失败:', error); + return { success: false, error: error.message }; + } + } + + // 获取消息的所有表情回应 + async getMessageReactions(messageId) { + try { + // 先从缓存获取 + const cached = this.reactionsCache.get(messageId); + if (cached && (Date.now() - cached.timestamp) < 60000) { // 1分钟缓存 + return { success: true, data: cached.reactions }; + } + + // 从服务器获取 + const response = await apiClient.request({ + url: `/api/v1/messages/${messageId}/reactions`, + method: 'GET' + }); + + if (response.success) { + const reactions = response.data || []; + + // 更新缓存 + this.reactionsCache.set(messageId, { + reactions: reactions, + timestamp: Date.now() + }); + + return { success: true, data: reactions }; + } else { + throw new Error(response.error || '获取表情回应失败'); + } + + } catch (error) { + console.error('❌ 获取表情回应失败:', error); + return { success: false, error: error.message }; + } + } + + // 获取用户对消息的表情回应 + async getUserReaction(messageId, userId) { + try { + const result = await this.getMessageReactions(messageId); + if (result.success) { + const userReaction = result.data.find(reaction => + reaction.users && reaction.users.includes(userId) + ); + return userReaction ? userReaction.emoji : null; + } + return null; + } catch (error) { + console.error('❌ 获取用户表情回应失败:', error); + return null; + } + } + + // 更新表情回应缓存 + updateReactionCache(messageId, emoji, userId, action) { + const cached = this.reactionsCache.get(messageId); + if (!cached) return; + + let reactions = cached.reactions; + let emojiReaction = reactions.find(r => r.emoji === emoji); + + if (action === 'add') { + if (emojiReaction) { + // 添加用户到现有表情 + if (!emojiReaction.users.includes(userId)) { + emojiReaction.users.push(userId); + emojiReaction.count = emojiReaction.users.length; + } + } else { + // 创建新的表情回应 + reactions.push({ + emoji: emoji, + count: 1, + users: [userId] + }); + } + } else if (action === 'remove') { + if (emojiReaction) { + // 从表情中移除用户 + emojiReaction.users = emojiReaction.users.filter(id => id !== userId); + emojiReaction.count = emojiReaction.users.length; + + // 如果没有用户了,移除整个表情 + if (emojiReaction.count === 0) { + reactions = reactions.filter(r => r.emoji !== emoji); + } + } + } + + // 更新缓存 + this.reactionsCache.set(messageId, { + reactions: reactions, + timestamp: Date.now() + }); + } + + // 💬 ===== 引用回复功能 ===== + + // 创建引用回复 + createQuoteReply(originalMessage, replyContent) { + try { + console.log('💬 创建引用回复:', originalMessage.messageId); + + // 生成引用内容 + const quoteContent = this.generateQuoteContent(originalMessage); + + // 构建引用回复消息 + const quoteReply = { + type: 'quote', + content: replyContent, + quote: { + messageId: originalMessage.messageId, + senderId: originalMessage.senderId, + senderName: originalMessage.senderName, + content: quoteContent, + timestamp: originalMessage.timestamp, + msgType: originalMessage.msgType + } + }; + + console.log('✅ 引用回复创建成功'); + return { success: true, data: quoteReply }; + + } catch (error) { + console.error('❌ 创建引用回复失败:', error); + return { success: false, error: error.message }; + } + } + + // 生成引用内容 + generateQuoteContent(message) { + let content = ''; + + switch (message.msgType) { + case 'text': + content = message.content; + // 限制长度 + if (content.length > this.interactionConfig.quote.maxQuoteLength) { + content = content.substring(0, this.interactionConfig.quote.maxQuoteLength) + '...'; + } + break; + case 'image': + content = '[图片]'; + break; + case 'video': + content = '[视频]'; + break; + case 'voice': + content = '[语音]'; + break; + case 'file': + content = '[文件]'; + break; + case 'location': + content = '[位置]'; + break; + default: + content = '[消息]'; + } + + return content; + } + + // 🔄 ===== 消息撤回功能 ===== + + // 撤回消息 + async recallMessage(messageId, userId) { + try { + console.log('🔄 撤回消息:', messageId, userId); + + // 检查撤回权限和时间限制 + const canRecall = await this.checkRecallPermission(messageId, userId); + if (!canRecall.allowed) { + return { success: false, error: canRecall.reason }; + } + + // 调用API撤回消息 + const response = await apiClient.request({ + url: `/api/v1/messages/${messageId}/recall`, + method: 'POST', + data: { + userId: userId + } + }); + + if (response.success) { + console.log('✅ 消息撤回成功'); + return { success: true, data: response.data }; + } else { + throw new Error(response.error || '撤回消息失败'); + } + + } catch (error) { + console.error('❌ 撤回消息失败:', error); + return { success: false, error: error.message }; + } + } + + // 检查撤回权限 + async checkRecallPermission(messageId, userId) { + try { + // 获取消息信息 + const messageInfo = await this.getMessageInfo(messageId); + if (!messageInfo) { + return { allowed: false, reason: '消息不存在' }; + } + + // 检查是否是消息发送者 + if (messageInfo.senderId !== userId) { + // 检查是否有管理员权限 + if (!this.interactionConfig.recall.allowRecallOthers) { + return { allowed: false, reason: '只能撤回自己的消息' }; + } + } + + // 检查时间限制 + const now = Date.now(); + const messageTime = messageInfo.timestamp; + const timeDiff = now - messageTime; + + if (timeDiff > this.interactionConfig.recall.timeLimit) { + return { allowed: false, reason: '超过撤回时间限制' }; + } + + return { allowed: true }; + + } catch (error) { + console.error('❌ 检查撤回权限失败:', error); + return { allowed: false, reason: '检查权限失败' }; + } + } + + // 📤 ===== 消息转发功能 ===== + + // 转发消息 + async forwardMessages(messageIds, targetConversations) { + try { + console.log('📤 转发消息:', messageIds, targetConversations); + + // 验证参数 + if (!messageIds.length || !targetConversations.length) { + throw new Error('参数不完整'); + } + + // 检查转发数量限制 + if (messageIds.length > this.interactionConfig.forward.maxForwardCount) { + throw new Error(`最多只能同时转发${this.interactionConfig.forward.maxForwardCount}条消息`); + } + + // 获取消息详情 + const messages = await this.getMessagesInfo(messageIds); + if (!messages.length) { + throw new Error('获取消息信息失败'); + } + + // 执行转发 + const results = []; + for (const targetId of targetConversations) { + const result = await this.forwardToConversation(messages, targetId); + results.push({ + targetId: targetId, + success: result.success, + error: result.error + }); + } + + console.log('✅ 消息转发完成'); + return { success: true, data: results }; + + } catch (error) { + console.error('❌ 转发消息失败:', error); + return { success: false, error: error.message }; + } + } + + // 转发到指定会话 + async forwardToConversation(messages, targetConversationId) { + try { + // 构建转发消息 + const forwardMessages = messages.map(msg => this.buildForwardMessage(msg)); + + // 调用API转发 + const response = await apiClient.request({ + url: '/api/v1/messages/forward', + method: 'POST', + data: { + messages: forwardMessages, + targetConversationId: targetConversationId, + preserveFormat: this.interactionConfig.forward.preserveFormat, + showSource: this.interactionConfig.forward.showForwardSource + } + }); + + if (response.success) { + return { success: true, data: response.data }; + } else { + throw new Error(response.error || '转发失败'); + } + + } catch (error) { + console.error('❌ 转发到会话失败:', error); + return { success: false, error: error.message }; + } + } + + // 构建转发消息 + buildForwardMessage(originalMessage) { + return { + originalMessageId: originalMessage.messageId, + content: originalMessage.content, + msgType: originalMessage.msgType, + originalSender: originalMessage.senderName, + originalTimestamp: originalMessage.timestamp, + forwardedAt: Date.now() + }; + } + + // ⭐ ===== 消息收藏功能 ===== + + // 收藏消息 + async favoriteMessage(messageId, userId) { + try { + console.log('⭐ 收藏消息:', messageId, userId); + + // 检查是否已收藏 + const isFavorited = await this.isMessageFavorited(messageId, userId); + if (isFavorited) { + return { success: false, error: '消息已收藏' }; + } + + // 调用API收藏 + const response = await apiClient.request({ + url: '/api/v1/messages/favorite', + method: 'POST', + data: { + messageId: messageId, + userId: userId + } + }); + + if (response.success) { + console.log('✅ 消息收藏成功'); + return { success: true, data: response.data }; + } else { + throw new Error(response.error || '收藏失败'); + } + + } catch (error) { + console.error('❌ 收藏消息失败:', error); + return { success: false, error: error.message }; + } + } + + // 取消收藏消息 + async unfavoriteMessage(messageId, userId) { + try { + console.log('⭐ 取消收藏消息:', messageId, userId); + + // 调用API取消收藏 + const response = await apiClient.request({ + url: `/api/v1/messages/favorite/${messageId}`, + method: 'DELETE', + data: { + userId: userId + } + }); + + if (response.success) { + console.log('✅ 取消收藏成功'); + return { success: true }; + } else { + throw new Error(response.error || '取消收藏失败'); + } + + } catch (error) { + console.error('❌ 取消收藏失败:', error); + return { success: false, error: error.message }; + } + } + + // 检查消息是否已收藏 + async isMessageFavorited(messageId, userId) { + try { + const response = await apiClient.request({ + url: `/api/v1/messages/favorite/check`, + method: 'GET', + data: { + messageId: messageId, + userId: userId + } + }); + + return response.success && response.data.favorited; + } catch (error) { + console.error('❌ 检查收藏状态失败:', error); + return false; + } + } + + // 📋 ===== 多选操作功能 ===== + + // 进入多选模式 + enterMultiSelectMode(initialMessageId = null) { + console.log('📋 进入多选模式'); + + this.multiSelectMode = true; + this.selectedMessages.clear(); + + if (initialMessageId) { + this.selectedMessages.add(initialMessageId); + } + + this.currentOperation = { + type: 'select', + messageId: null, + data: { + selectedCount: this.selectedMessages.size + } + }; + } + + // 退出多选模式 + exitMultiSelectMode() { + console.log('📋 退出多选模式'); + + this.multiSelectMode = false; + this.selectedMessages.clear(); + this.currentOperation = { + type: null, + messageId: null, + data: null + }; + } + + // 切换消息选择状态 + toggleMessageSelection(messageId) { + if (this.selectedMessages.has(messageId)) { + this.selectedMessages.delete(messageId); + } else { + this.selectedMessages.add(messageId); + } + + this.currentOperation.data.selectedCount = this.selectedMessages.size; + + console.log('📋 消息选择状态切换:', messageId, this.selectedMessages.size); + } + + // 获取选中的消息 + getSelectedMessages() { + return Array.from(this.selectedMessages); + } + + // 🔧 ===== 工具方法 ===== + + // 获取消息信息 + async getMessageInfo(messageId) { + try { + const response = await apiClient.request({ + url: `/api/v1/messages/${messageId}`, + method: 'GET' + }); + + return response.success ? response.data : null; + } catch (error) { + console.error('❌ 获取消息信息失败:', error); + return null; + } + } + + // 获取多条消息信息 + async getMessagesInfo(messageIds) { + try { + const response = await apiClient.request({ + url: '/api/v1/messages/batch', + method: 'POST', + data: { + messageIds: messageIds + } + }); + + return response.success ? response.data : []; + } catch (error) { + console.error('❌ 获取消息信息失败:', error); + return []; + } + } + + // 加载用户收藏 + async loadUserFavorites() { + try { + const favorites = wx.getStorageSync('userFavorites') || []; + this.userFavorites = new Set(favorites); + } catch (error) { + console.error('❌ 加载用户收藏失败:', error); + } + } + + // 加载表情回应缓存 + async loadReactionsCache() { + try { + const cached = wx.getStorageSync('reactionsCache') || {}; + this.reactionsCache = new Map(Object.entries(cached)); + } catch (error) { + console.error('❌ 加载表情回应缓存失败:', error); + } + } + + // 保存表情回应缓存 + saveReactionsCache() { + try { + const cacheObj = Object.fromEntries(this.reactionsCache); + wx.setStorageSync('reactionsCache', cacheObj); + } catch (error) { + console.error('❌ 保存表情回应缓存失败:', error); + } + } + + // 获取当前操作状态 + getCurrentOperation() { + return { ...this.currentOperation }; + } + + // 获取交互配置 + getInteractionConfig() { + return { ...this.interactionConfig }; + } + + // 重置管理器 + reset() { + this.exitMultiSelectMode(); + this.reactionsCache.clear(); + this.currentOperation = { + type: null, + messageId: null, + data: null + }; + } +} + +// 创建全局实例 +const messageInteractionManager = new MessageInteractionManager(); + +module.exports = messageInteractionManager; diff --git a/utils/message-search-manager.js b/utils/message-search-manager.js new file mode 100644 index 0000000..504babd --- /dev/null +++ b/utils/message-search-manager.js @@ -0,0 +1,484 @@ +// 消息搜索管理器 - 微信小程序专用 +// 处理消息搜索、历史记录管理、本地缓存等 + +const apiClient = require('./api-client.js'); + +/** + * 消息搜索管理器 + * 功能: + * 1. 全局消息搜索 + * 2. 会话内搜索 + * 3. 搜索结果高亮 + * 4. 搜索历史管理 + * 5. 本地缓存优化 + * 6. 分页加载 + */ +class MessageSearchManager { + constructor() { + this.isInitialized = false; + + // 搜索配置 + this.searchConfig = { + // 最小搜索关键词长度 + minKeywordLength: 1, + + // 搜索结果每页数量 + pageSize: 20, + + // 最大搜索历史数量 + maxSearchHistory: 50, + + // 本地缓存过期时间(毫秒) + cacheExpireTime: 30 * 60 * 1000, // 30分钟 + + // 搜索防抖延迟(毫秒) + debounceDelay: 300, + + // 支持的搜索类型 + searchTypes: ['text', 'image', 'file', 'all'] + }; + + // 搜索缓存 + this.searchCache = new Map(); + + // 搜索历史 + this.searchHistory = []; + + // 当前搜索状态 + this.currentSearch = { + keyword: '', + type: 'all', + conversationId: null, + page: 1, + hasMore: true, + loading: false, + results: [] + }; + + // 防抖定时器 + this.debounceTimer = null; + + this.init(); + } + + // 初始化搜索管理器 + async init() { + if (this.isInitialized) return; + + console.log('🔍 初始化消息搜索管理器...'); + + try { + // 加载搜索历史 + await this.loadSearchHistory(); + + // 清理过期缓存 + this.cleanupExpiredCache(); + + this.isInitialized = true; + console.log('✅ 消息搜索管理器初始化完成'); + + } catch (error) { + console.error('❌ 消息搜索管理器初始化失败:', error); + } + } + + // 全局搜索消息 + async searchMessages(keyword, options = {}) { + try { + // 验证搜索关键词 + if (!this.validateKeyword(keyword)) { + return { success: false, error: '搜索关键词无效' }; + } + + console.log('🔍 搜索消息:', keyword); + + // 设置搜索参数 + const searchParams = { + keyword: keyword.trim(), + type: options.type || 'all', + conversationId: options.conversationId || null, + page: options.page || 1, + pageSize: options.pageSize || this.searchConfig.pageSize + }; + + // 检查缓存 + const cacheKey = this.generateCacheKey(searchParams); + const cachedResult = this.getFromCache(cacheKey); + if (cachedResult) { + console.log('🔍 使用缓存搜索结果'); + return cachedResult; + } + + // 更新搜索状态 + this.updateSearchState(searchParams); + + // 执行搜索 + const result = await this.performSearch(searchParams); + + // 缓存结果 + if (result.success) { + this.saveToCache(cacheKey, result); + + // 添加到搜索历史 + this.addToSearchHistory(keyword); + } + + return result; + + } catch (error) { + console.error('❌ 搜索消息失败:', error); + return { success: false, error: error.message }; + } + } + + // 会话内搜索 + async searchInConversation(conversationId, keyword, options = {}) { + return await this.searchMessages(keyword, { + ...options, + conversationId: conversationId + }); + } + + // 防抖搜索 + searchWithDebounce(keyword, options = {}, callback) { + // 清除之前的定时器 + if (this.debounceTimer) { + clearTimeout(this.debounceTimer); + } + + // 设置新的定时器 + this.debounceTimer = setTimeout(async () => { + try { + const result = await this.searchMessages(keyword, options); + if (callback) { + callback(result); + } + } catch (error) { + console.error('❌ 防抖搜索失败:', error); + if (callback) { + callback({ success: false, error: error.message }); + } + } + }, this.searchConfig.debounceDelay); + } + + // 执行搜索 + async performSearch(searchParams) { + try { + this.currentSearch.loading = true; + + // 构建搜索请求 + const requestData = { + keyword: searchParams.keyword, + messageType: searchParams.type === 'all' ? null : this.getMessageTypeCode(searchParams.type), + page: searchParams.page, + pageSize: searchParams.pageSize + }; + + // 如果指定了会话ID,则进行会话内搜索 + if (searchParams.conversationId) { + requestData.conversationId = searchParams.conversationId; + } + + // 调用搜索API(按统一客户端签名) + const response = await apiClient.post('/api/v1/messages/search', requestData); + + if (response && (response.code === 0 || response.code === 200 || response.success)) { + const searchResult = { + success: true, + data: { + messages: response.data?.messages || [], + total: response.data?.total || 0, + page: searchParams.page, + pageSize: searchParams.pageSize, + hasMore: response.data?.hasMore || false, + keyword: searchParams.keyword, + searchTime: Date.now() + } + }; + + // 处理搜索结果 + searchResult.data.messages = this.processSearchResults( + searchResult.data.messages, + searchParams.keyword + ); + + console.log(`🔍 搜索完成,找到 ${searchResult.data.total} 条消息`); + return searchResult; + + } else { + const msg = response?.message || response?.error || '搜索失败'; + throw new Error(msg); + } + + } catch (error) { + console.error('❌ 执行搜索失败:', error); + return { success: false, error: error.message }; + + } finally { + this.currentSearch.loading = false; + } + } + + // 处理搜索结果 + processSearchResults(messages, keyword) { + return messages.map(message => { + // 添加高亮信息 + const highlightedContent = this.highlightKeyword(message.content, keyword); + + return { + ...message, + highlightedContent: highlightedContent, + searchKeyword: keyword, + // 添加消息摘要(用于显示上下文) + summary: this.generateMessageSummary(message.content, keyword) + }; + }); + } + + // 关键词高亮 + highlightKeyword(content, keyword) { + if (!content || !keyword) return content; + + try { + // 转义特殊字符 + const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regex = new RegExp(`(${escapedKeyword})`, 'gi'); + + return content.replace(regex, '$1'); + + } catch (error) { + console.error('❌ 关键词高亮失败:', error); + return content; + } + } + + // 生成消息摘要 + generateMessageSummary(content, keyword, maxLength = 100) { + if (!content || !keyword) return content; + + try { + const keywordIndex = content.toLowerCase().indexOf(keyword.toLowerCase()); + if (keywordIndex === -1) return content.substring(0, maxLength); + + // 计算摘要范围 + const start = Math.max(0, keywordIndex - 30); + const end = Math.min(content.length, keywordIndex + keyword.length + 30); + + let summary = content.substring(start, end); + + // 添加省略号 + if (start > 0) summary = '...' + summary; + if (end < content.length) summary = summary + '...'; + + return summary; + + } catch (error) { + console.error('❌ 生成消息摘要失败:', error); + return content.substring(0, maxLength); + } + } + + // 加载更多搜索结果 + async loadMoreResults() { + if (!this.currentSearch.hasMore || this.currentSearch.loading) { + return { success: false, error: '没有更多结果' }; + } + + const nextPage = this.currentSearch.page + 1; + const result = await this.searchMessages(this.currentSearch.keyword, { + type: this.currentSearch.type, + conversationId: this.currentSearch.conversationId, + page: nextPage + }); + + if (result.success) { + // 合并结果 + this.currentSearch.results = [...this.currentSearch.results, ...result.data.messages]; + this.currentSearch.page = nextPage; + this.currentSearch.hasMore = result.data.hasMore; + } + + return result; + } + + // 清除搜索结果 + clearSearchResults() { + this.currentSearch = { + keyword: '', + type: 'all', + conversationId: null, + page: 1, + hasMore: true, + loading: false, + results: [] + }; + } + + // 验证搜索关键词 + validateKeyword(keyword) { + if (!keyword || typeof keyword !== 'string') { + return false; + } + + const trimmedKeyword = keyword.trim(); + return trimmedKeyword.length >= this.searchConfig.minKeywordLength; + } + + // 获取消息类型代码 + getMessageTypeCode(type) { + const typeMap = { + 'text': 0, + 'image': 1, + 'voice': 2, + 'video': 3, + 'file': 4 + }; + return typeMap[type] || null; + } + + // 更新搜索状态 + updateSearchState(searchParams) { + this.currentSearch = { + keyword: searchParams.keyword, + type: searchParams.type, + conversationId: searchParams.conversationId, + page: searchParams.page, + hasMore: true, + loading: true, + results: [] + }; + } + + // 生成缓存键 + generateCacheKey(searchParams) { + return `search_${searchParams.keyword}_${searchParams.type}_${searchParams.conversationId || 'global'}_${searchParams.page}`; + } + + // 从缓存获取 + getFromCache(cacheKey) { + const cached = this.searchCache.get(cacheKey); + if (cached && (Date.now() - cached.timestamp) < this.searchConfig.cacheExpireTime) { + return cached.data; + } + return null; + } + + // 保存到缓存 + saveToCache(cacheKey, data) { + this.searchCache.set(cacheKey, { + data: data, + timestamp: Date.now() + }); + + // 限制缓存大小 + if (this.searchCache.size > 100) { + const firstKey = this.searchCache.keys().next().value; + this.searchCache.delete(firstKey); + } + } + + // 清理过期缓存 + cleanupExpiredCache() { + const now = Date.now(); + for (const [key, value] of this.searchCache) { + if (now - value.timestamp > this.searchConfig.cacheExpireTime) { + this.searchCache.delete(key); + } + } + } + + // 添加到搜索历史 + addToSearchHistory(keyword) { + const trimmedKeyword = keyword.trim(); + if (!trimmedKeyword) return; + + // 移除重复项 + this.searchHistory = this.searchHistory.filter(item => item !== trimmedKeyword); + + // 添加到开头 + this.searchHistory.unshift(trimmedKeyword); + + // 限制历史数量 + if (this.searchHistory.length > this.searchConfig.maxSearchHistory) { + this.searchHistory = this.searchHistory.slice(0, this.searchConfig.maxSearchHistory); + } + + // 保存到本地存储 + this.saveSearchHistory(); + } + + // 获取搜索历史 + getSearchHistory() { + return [...this.searchHistory]; + } + + // 清除搜索历史 + clearSearchHistory() { + this.searchHistory = []; + this.saveSearchHistory(); + } + + // 删除搜索历史项 + removeSearchHistoryItem(keyword) { + this.searchHistory = this.searchHistory.filter(item => item !== keyword); + this.saveSearchHistory(); + } + + // 加载搜索历史 + async loadSearchHistory() { + try { + const history = wx.getStorageSync('messageSearchHistory') || []; + this.searchHistory = history; + } catch (error) { + console.error('❌ 加载搜索历史失败:', error); + } + } + + // 保存搜索历史 + async saveSearchHistory() { + try { + wx.setStorageSync('messageSearchHistory', this.searchHistory); + } catch (error) { + console.error('❌ 保存搜索历史失败:', error); + } + } + + // 获取搜索建议 + getSearchSuggestions(keyword) { + if (!keyword) return this.searchHistory.slice(0, 10); + + const lowerKeyword = keyword.toLowerCase(); + return this.searchHistory + .filter(item => item.toLowerCase().includes(lowerKeyword)) + .slice(0, 10); + } + + // 获取当前搜索状态 + getCurrentSearchState() { + return { ...this.currentSearch }; + } + + // 获取搜索统计 + getSearchStats() { + return { + historyCount: this.searchHistory.length, + cacheSize: this.searchCache.size, + currentKeyword: this.currentSearch.keyword, + isLoading: this.currentSearch.loading, + hasResults: this.currentSearch.results.length > 0 + }; + } + + // 重置搜索管理器 + reset() { + this.searchCache.clear(); + this.clearSearchResults(); + this.clearSearchHistory(); + } +} + +// 创建全局实例 +const messageSearchManager = new MessageSearchManager(); + +module.exports = messageSearchManager; diff --git a/utils/message-sync-manager.js b/utils/message-sync-manager.js new file mode 100644 index 0000000..9fc8709 --- /dev/null +++ b/utils/message-sync-manager.js @@ -0,0 +1,614 @@ +// 消息状态同步管理器 - 微信小程序专用 +// 处理消息的已读状态、多端同步、离线消息等 + +const wsManager = require('./websocket-manager-v2.js'); +const apiClient = require('./api-client.js'); + +/** + * 消息状态同步管理器 + * 功能: + * 1. 消息已读状态同步 + * 2. 多端消息状态同步 + * 3. 离线消息处理 + * 4. 消息状态缓存 + * 5. 网络状态处理 + */ +class MessageSyncManager { + constructor() { + this.isInitialized = false; + + // 消息状态缓存 + this.messageStatusCache = new Map(); + + // 待同步的状态队列 + this.pendingSyncQueue = []; + + // 同步配置 + this.syncConfig = { + // 批量同步的最大数量 + maxBatchSize: 50, + + // 同步间隔(毫秒) + syncInterval: 5000, + + // 重试配置 + maxRetries: 3, + retryDelay: 2000, + + // 缓存过期时间(毫秒) + cacheExpireTime: 30 * 60 * 1000 // 30分钟 + }; + + // 同步状态 + this.syncStatus = { + isOnline: false, + lastSyncTime: 0, + pendingCount: 0, + failedCount: 0 + }; + + // 定时器 + this.syncTimer = null; + this.retryTimer = null; + + this.init(); + } + + // 初始化同步管理器 + async init() { + if (this.isInitialized) return; + + console.log('🔄 初始化消息状态同步管理器...'); + + try { + // 加载缓存的消息状态 + await this.loadMessageStatusCache(); + + // 加载待同步队列 + await this.loadPendingSyncQueue(); + + // 注册WebSocket事件 + this.registerWebSocketEvents(); + + // 注册网络状态监听 + this.registerNetworkEvents(); + + // 启动定时同步 + this.startSyncTimer(); + + this.isInitialized = true; + console.log('✅ 消息状态同步管理器初始化完成'); + + } catch (error) { + console.error('❌ 消息状态同步管理器初始化失败:', error); + } + } + + // 注册WebSocket事件 + registerWebSocketEvents() { + // WebSocket连接成功 + wsManager.on('connected', () => { + console.log('🔄 WebSocket连接成功,开始同步消息状态'); + this.syncStatus.isOnline = true; + this.processPendingSyncQueue(); + }); + + // WebSocket连接断开 + wsManager.on('disconnected', () => { + console.log('🔄 WebSocket连接断开,切换到离线模式'); + this.syncStatus.isOnline = false; + }); + + // 接收消息状态更新 + wsManager.on('message', (data) => { + this.handleWebSocketMessage(data); + }); + } + + // 处理WebSocket消息 + async handleWebSocketMessage(data) { + try { + const message = typeof data === 'string' ? JSON.parse(data) : data; + + switch (message.type) { + case 'message_read': + await this.handleMessageReadUpdate(message.data); + break; + case 'message_status_sync': + await this.handleMessageStatusSync(message.data); + break; + case 'conversation_read': + await this.handleConversationReadUpdate(message.data); + break; + } + + } catch (error) { + console.error('❌ 处理WebSocket消息状态更新失败:', error); + } + } + + // 处理消息已读更新 + async handleMessageReadUpdate(data) { + console.log('👁️ 收到消息已读更新:', data); + + // 更新本地缓存 + if (data.messageIds && Array.isArray(data.messageIds)) { + for (const messageId of data.messageIds) { + this.updateMessageStatus(messageId, 'read', data.readTime); + } + } + + // 触发页面更新事件 + this.triggerEvent('message_read_updated', data); + } + + // 处理会话已读更新 + async handleConversationReadUpdate(data) { + console.log('👁️ 收到会话已读更新:', data); + + // 更新会话中所有消息的状态 + if (data.conversationId && data.lastReadMessageId) { + await this.markConversationAsRead(data.conversationId, data.lastReadMessageId); + } + + // 触发页面更新事件 + this.triggerEvent('conversation_read_updated', data); + } + + // 处理消息状态同步 + async handleMessageStatusSync(data) { + console.log('🔄 收到消息状态同步:', data); + + if (data.messageStatuses && Array.isArray(data.messageStatuses)) { + for (const status of data.messageStatuses) { + this.updateMessageStatus(status.messageId, status.status, status.timestamp); + } + } + + // 保存缓存 + await this.saveMessageStatusCache(); + } + + // 标记消息为已读 + async markMessageAsRead(messageId, conversationId) { + try { + console.log('👁️ 标记消息为已读:', messageId); + + // 更新本地状态 + this.updateMessageStatus(messageId, 'read', Date.now()); + + // 添加到同步队列 + this.addToSyncQueue({ + type: 'message_read', + messageId: messageId, + conversationId: conversationId, + timestamp: Date.now() + }); + + // 立即尝试同步(如果在线) + if (this.syncStatus.isOnline) { + await this.processPendingSyncQueue(); + } + + return true; + + } catch (error) { + console.error('❌ 标记消息已读失败:', error); + return false; + } + } + + // 标记会话为已读 + async markConversationAsRead(conversationId, lastReadMessageId) { + try { + console.log('👁️ 标记会话为已读:', conversationId); + + // 添加到同步队列 + this.addToSyncQueue({ + type: 'conversation_read', + conversationId: conversationId, + lastReadMessageId: lastReadMessageId, + timestamp: Date.now() + }); + + // 立即尝试同步(如果在线) + if (this.syncStatus.isOnline) { + await this.processPendingSyncQueue(); + } + + return true; + + } catch (error) { + console.error('❌ 标记会话已读失败:', error); + return false; + } + } + + // 批量标记消息为已读 + async markMessagesAsRead(messageIds, conversationId) { + try { + console.log('👁️ 批量标记消息为已读:', messageIds.length); + + const timestamp = Date.now(); + + // 更新本地状态 + for (const messageId of messageIds) { + this.updateMessageStatus(messageId, 'read', timestamp); + } + + // 添加到同步队列 + this.addToSyncQueue({ + type: 'batch_message_read', + messageIds: messageIds, + conversationId: conversationId, + timestamp: timestamp + }); + + // 立即尝试同步(如果在线) + if (this.syncStatus.isOnline) { + await this.processPendingSyncQueue(); + } + + return true; + + } catch (error) { + console.error('❌ 批量标记消息已读失败:', error); + return false; + } + } + + // 更新消息状态 + updateMessageStatus(messageId, status, timestamp) { + this.messageStatusCache.set(messageId, { + status: status, + timestamp: timestamp, + synced: false + }); + } + + // 获取消息状态 + getMessageStatus(messageId) { + return this.messageStatusCache.get(messageId); + } + + // 检查消息是否已读 + isMessageRead(messageId) { + const status = this.getMessageStatus(messageId); + return status && status.status === 'read'; + } + + // 添加到同步队列 + addToSyncQueue(syncItem) { + this.pendingSyncQueue.push({ + ...syncItem, + id: this.generateSyncId(), + retries: 0, + addedTime: Date.now() + }); + + this.syncStatus.pendingCount = this.pendingSyncQueue.length; + this.savePendingSyncQueue(); + } + + // 处理待同步队列 + async processPendingSyncQueue() { + if (this.pendingSyncQueue.length === 0) { + return; + } + + console.log(`🔄 处理待同步队列,共 ${this.pendingSyncQueue.length} 项`); + + // 按类型分组批量处理 + const groupedItems = this.groupSyncItemsByType(); + + for (const [type, items] of groupedItems) { + try { + await this.syncItemsByType(type, items); + } catch (error) { + console.error(`❌ 同步 ${type} 类型失败:`, error); + this.handleSyncFailure(items, error); + } + } + + // 清理已同步的项目 + this.cleanupSyncedItems(); + + // 更新同步状态 + this.syncStatus.lastSyncTime = Date.now(); + this.syncStatus.pendingCount = this.pendingSyncQueue.length; + } + + // 按类型分组同步项目 + groupSyncItemsByType() { + const groups = new Map(); + + for (const item of this.pendingSyncQueue) { + if (!groups.has(item.type)) { + groups.set(item.type, []); + } + groups.get(item.type).push(item); + } + + return groups; + } + + // 按类型同步项目 + async syncItemsByType(type, items) { + switch (type) { + case 'message_read': + await this.syncMessageRead(items); + break; + case 'batch_message_read': + await this.syncBatchMessageRead(items); + break; + case 'conversation_read': + await this.syncConversationRead(items); + break; + default: + console.warn('🔄 未知的同步类型:', type); + } + } + + // 同步单个消息已读 + async syncMessageRead(items) { + const messageIds = items.map(item => item.messageId); + + const response = await apiClient.request({ + url: '/api/v1/messages/mark-read', + method: 'POST', + data: { + messageIds: messageIds + } + }); + + if (response.success) { + // 标记为已同步 + items.forEach(item => { + item.synced = true; + // 更新缓存中的同步状态 + const status = this.messageStatusCache.get(item.messageId); + if (status) { + status.synced = true; + } + }); + + console.log(`✅ 同步 ${items.length} 个消息已读状态成功`); + } else { + throw new Error(response.error || '同步失败'); + } + } + + // 同步批量消息已读 + async syncBatchMessageRead(items) { + for (const item of items) { + const response = await apiClient.request({ + url: '/api/v1/messages/batch-mark-read', + method: 'POST', + data: { + messageIds: item.messageIds, + conversationId: item.conversationId + } + }); + + if (response.success) { + item.synced = true; + // 更新缓存中的同步状态 + item.messageIds.forEach(messageId => { + const status = this.messageStatusCache.get(messageId); + if (status) { + status.synced = true; + } + }); + } else { + throw new Error(response.error || '批量同步失败'); + } + } + + console.log(`✅ 同步 ${items.length} 个批量消息已读状态成功`); + } + + // 同步会话已读 + async syncConversationRead(items) { + for (const item of items) { + const response = await apiClient.request({ + url: '/api/v1/conversations/mark-read', + method: 'POST', + data: { + conversationId: item.conversationId, + lastReadMessageId: item.lastReadMessageId + } + }); + + if (response.success) { + item.synced = true; + } else { + throw new Error(response.error || '会话同步失败'); + } + } + + console.log(`✅ 同步 ${items.length} 个会话已读状态成功`); + } + + // 处理同步失败 + handleSyncFailure(items, error) { + for (const item of items) { + item.retries++; + if (item.retries >= this.syncConfig.maxRetries) { + console.error(`❌ 同步项目 ${item.id} 达到最大重试次数,放弃同步`); + item.failed = true; + this.syncStatus.failedCount++; + } + } + } + + // 清理已同步的项目 + cleanupSyncedItems() { + const beforeCount = this.pendingSyncQueue.length; + this.pendingSyncQueue = this.pendingSyncQueue.filter(item => !item.synced && !item.failed); + const afterCount = this.pendingSyncQueue.length; + + if (beforeCount !== afterCount) { + console.log(`🧹 清理了 ${beforeCount - afterCount} 个已同步项目`); + this.savePendingSyncQueue(); + } + } + + // 生成同步ID + generateSyncId() { + return `sync_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + // 启动定时同步 + startSyncTimer() { + if (this.syncTimer) { + clearInterval(this.syncTimer); + } + + this.syncTimer = setInterval(() => { + if (this.syncStatus.isOnline && this.pendingSyncQueue.length > 0) { + this.processPendingSyncQueue(); + } + }, this.syncConfig.syncInterval); + } + + // 停止定时同步 + stopSyncTimer() { + if (this.syncTimer) { + clearInterval(this.syncTimer); + this.syncTimer = null; + } + } + + // 注册网络状态监听 + registerNetworkEvents() { + wx.onNetworkStatusChange((res) => { + if (res.isConnected && !this.syncStatus.isOnline) { + console.log('🌐 网络连接恢复,尝试同步消息状态'); + this.syncStatus.isOnline = true; + this.processPendingSyncQueue(); + } else if (!res.isConnected) { + console.log('🌐 网络连接断开,切换到离线模式'); + this.syncStatus.isOnline = false; + } + }); + } + + // 加载消息状态缓存 + async loadMessageStatusCache() { + try { + const cache = wx.getStorageSync('messageStatusCache') || {}; + this.messageStatusCache = new Map(Object.entries(cache)); + + // 清理过期缓存 + this.cleanupExpiredCache(); + + } catch (error) { + console.error('❌ 加载消息状态缓存失败:', error); + } + } + + // 保存消息状态缓存 + async saveMessageStatusCache() { + try { + const cache = Object.fromEntries(this.messageStatusCache); + wx.setStorageSync('messageStatusCache', cache); + } catch (error) { + console.error('❌ 保存消息状态缓存失败:', error); + } + } + + // 清理过期缓存 + cleanupExpiredCache() { + const now = Date.now(); + const expireTime = this.syncConfig.cacheExpireTime; + + for (const [messageId, status] of this.messageStatusCache) { + if (now - status.timestamp > expireTime) { + this.messageStatusCache.delete(messageId); + } + } + } + + // 加载待同步队列 + async loadPendingSyncQueue() { + try { + this.pendingSyncQueue = wx.getStorageSync('pendingSyncQueue') || []; + this.syncStatus.pendingCount = this.pendingSyncQueue.length; + } catch (error) { + console.error('❌ 加载待同步队列失败:', error); + } + } + + // 保存待同步队列 + async savePendingSyncQueue() { + try { + wx.setStorageSync('pendingSyncQueue', this.pendingSyncQueue); + } catch (error) { + console.error('❌ 保存待同步队列失败:', error); + } + } + + // 事件处理器 + eventHandlers = new Map(); + + // 注册事件监听器 + on(event, handler) { + if (!this.eventHandlers.has(event)) { + this.eventHandlers.set(event, []); + } + this.eventHandlers.get(event).push(handler); + } + + // 触发事件 + triggerEvent(event, data) { + const handlers = this.eventHandlers.get(event); + if (handlers) { + handlers.forEach(handler => { + try { + handler(data); + } catch (error) { + console.error(`❌ 事件处理器错误 [${event}]:`, error); + } + }); + } + } + + // 获取同步状态 + getSyncStatus() { + return { + ...this.syncStatus, + cacheSize: this.messageStatusCache.size, + pendingItems: this.pendingSyncQueue.length + }; + } + + // 强制同步 + async forceSync() { + console.log('🔄 强制同步消息状态...'); + await this.processPendingSyncQueue(); + } + + // 清理所有数据 + reset() { + this.messageStatusCache.clear(); + this.pendingSyncQueue = []; + this.syncStatus.pendingCount = 0; + this.syncStatus.failedCount = 0; + + this.saveMessageStatusCache(); + this.savePendingSyncQueue(); + } + + // 销毁管理器 + destroy() { + this.stopSyncTimer(); + this.reset(); + this.eventHandlers.clear(); + this.isInitialized = false; + } +} + +// 创建全局实例 +const messageSyncManager = new MessageSyncManager(); + +module.exports = messageSyncManager; diff --git a/utils/network-optimizer.js b/utils/network-optimizer.js new file mode 100644 index 0000000..8e64477 --- /dev/null +++ b/utils/network-optimizer.js @@ -0,0 +1,684 @@ +// 网络优化管理器 - 微信小程序专用 +// 提供网络请求优化、缓存管理、离线支持等功能 + +const errorHandler = require('./error-handler.js'); +const performanceMonitor = require('./performance-monitor.js'); + +/** + * 网络优化管理器 + * 功能: + * 1. 请求缓存和优化 + * 2. 离线数据支持 + * 3. 网络状态监控 + * 4. 请求队列管理 + * 5. 断网重连机制 + * 6. 数据压缩和优化 + */ +class NetworkOptimizer { + constructor() { + this.isInitialized = false; + + // 网络优化配置 + this.config = { + // 缓存配置 + cache: { + enabled: true, + maxSize: 50 * 1024 * 1024, // 50MB + defaultTTL: 300000, // 5分钟 + maxAge: 24 * 60 * 60 * 1000, // 24小时 + compressionEnabled: true + }, + + // 离线配置 + offline: { + enabled: true, + maxOfflineData: 10 * 1024 * 1024, // 10MB + syncOnReconnect: true + }, + + // 请求配置 + request: { + timeout: 10000, // 10秒超时 + maxConcurrent: 6, // 最大并发请求数 + retryAttempts: 3, // 重试次数 + retryDelay: 1000, // 重试延迟 + enableCompression: true, // 启用压缩 + enableKeepAlive: true // 启用长连接 + }, + + // 预加载配置 + preload: { + enabled: true, + maxPreloadSize: 5 * 1024 * 1024, // 5MB + preloadDelay: 2000 // 预加载延迟 + } + }; + + // 网络状态 + this.networkState = { + isOnline: true, + networkType: 'unknown', + lastOnlineTime: Date.now(), + connectionQuality: 'good' + }; + + // 缓存存储 + this.cache = new Map(); + this.cacheMetadata = new Map(); + + // 离线数据队列 + this.offlineQueue = []; + + // 请求队列 + this.requestQueue = []; + this.activeRequests = new Set(); + + // 预加载队列 + this.preloadQueue = []; + + // 统计数据 + this.stats = { + totalRequests: 0, + cachedRequests: 0, + failedRequests: 0, + offlineRequests: 0, + averageResponseTime: 0, + cacheHitRate: 0 + }; + + this.init(); + } + + // 初始化网络优化器 + async init() { + if (this.isInitialized || !this.config.cache.enabled) return; + + console.log('🌐 初始化网络优化器...'); + + try { + // 获取网络状态 + await this.updateNetworkState(); + + // 加载缓存数据 + await this.loadCacheFromStorage(); + + // 设置网络监听 + this.setupNetworkListeners(); + + // 启动请求队列处理 + this.startRequestQueueProcessor(); + + // 启动缓存清理 + this.startCacheCleanup(); + + // 启动预加载处理 + this.startPreloadProcessor(); + + this.isInitialized = true; + console.log('✅ 网络优化器初始化完成'); + + } catch (error) { + console.error('❌ 网络优化器初始化失败:', error); + } + } + + // 🌐 ===== 网络状态管理 ===== + + // 更新网络状态 + async updateNetworkState() { + try { + const networkInfo = await new Promise((resolve, reject) => { + wx.getNetworkType({ + success: resolve, + fail: reject + }); + }); + + const wasOnline = this.networkState.isOnline; + this.networkState.isOnline = networkInfo.networkType !== 'none'; + this.networkState.networkType = networkInfo.networkType; + + if (this.networkState.isOnline) { + this.networkState.lastOnlineTime = Date.now(); + + // 如果从离线恢复到在线,处理离线队列 + if (!wasOnline && this.config.offline.syncOnReconnect) { + this.processOfflineQueue(); + } + } + + // 更新连接质量 + this.updateConnectionQuality(); + + } catch (error) { + console.error('❌ 更新网络状态失败:', error); + } + } + + // 更新连接质量 + updateConnectionQuality() { + const networkType = this.networkState.networkType; + + if (networkType === 'wifi') { + this.networkState.connectionQuality = 'excellent'; + } else if (networkType === '4g') { + this.networkState.connectionQuality = 'good'; + } else if (networkType === '3g') { + this.networkState.connectionQuality = 'fair'; + } else if (networkType === '2g') { + this.networkState.connectionQuality = 'poor'; + } else { + this.networkState.connectionQuality = 'unknown'; + } + } + + // 设置网络监听 + setupNetworkListeners() { + wx.onNetworkStatusChange((res) => { + console.log('🌐 网络状态变化:', res); + + this.networkState.isOnline = res.isConnected; + this.networkState.networkType = res.networkType; + + if (res.isConnected) { + this.networkState.lastOnlineTime = Date.now(); + + // 网络恢复,处理离线队列 + if (this.config.offline.syncOnReconnect) { + this.processOfflineQueue(); + } + } + + this.updateConnectionQuality(); + }); + } + + // 📦 ===== 缓存管理 ===== + + // 获取缓存 + getCache(key) { + if (!this.config.cache.enabled) return null; + + const cached = this.cache.get(key); + const metadata = this.cacheMetadata.get(key); + + if (!cached || !metadata) return null; + + // 检查缓存是否过期 + if (Date.now() - metadata.timestamp > metadata.ttl) { + this.cache.delete(key); + this.cacheMetadata.delete(key); + return null; + } + + // 更新访问时间 + metadata.lastAccess = Date.now(); + metadata.accessCount++; + + console.log('📦 缓存命中:', key); + this.stats.cachedRequests++; + + return cached; + } + + // 设置缓存 + setCache(key, data, ttl = this.config.cache.defaultTTL) { + if (!this.config.cache.enabled) return; + + try { + // 检查缓存大小 + if (this.getCacheSize() > this.config.cache.maxSize) { + this.cleanupCache(); + } + + // 压缩数据(如果启用) + const compressedData = this.config.cache.compressionEnabled ? + this.compressData(data) : data; + + this.cache.set(key, compressedData); + this.cacheMetadata.set(key, { + timestamp: Date.now(), + ttl: ttl, + size: this.getDataSize(compressedData), + lastAccess: Date.now(), + accessCount: 1, + compressed: this.config.cache.compressionEnabled + }); + + console.log('📦 缓存设置:', key); + + // 异步保存到本地存储 + this.saveCacheToStorage(); + + } catch (error) { + console.error('❌ 设置缓存失败:', error); + } + } + + // 删除缓存 + deleteCache(key) { + this.cache.delete(key); + this.cacheMetadata.delete(key); + this.saveCacheToStorage(); + } + + // 清空缓存 + clearCache() { + this.cache.clear(); + this.cacheMetadata.clear(); + wx.removeStorageSync('network_cache'); + wx.removeStorageSync('cache_metadata'); + console.log('🧹 缓存已清空'); + } + + // 获取缓存大小 + getCacheSize() { + let totalSize = 0; + for (const metadata of this.cacheMetadata.values()) { + totalSize += metadata.size; + } + return totalSize; + } + + // 清理过期缓存 + cleanupCache() { + const now = Date.now(); + const keysToDelete = []; + + for (const [key, metadata] of this.cacheMetadata.entries()) { + // 删除过期的缓存 + if (now - metadata.timestamp > metadata.ttl || + now - metadata.timestamp > this.config.cache.maxAge) { + keysToDelete.push(key); + } + } + + // 如果还是太大,删除最少使用的缓存 + if (this.getCacheSize() > this.config.cache.maxSize) { + const sortedEntries = Array.from(this.cacheMetadata.entries()) + .sort((a, b) => a[1].lastAccess - b[1].lastAccess); + + const halfSize = Math.floor(sortedEntries.length / 2); + for (let i = 0; i < halfSize; i++) { + keysToDelete.push(sortedEntries[i][0]); + } + } + + // 删除缓存 + keysToDelete.forEach(key => { + this.cache.delete(key); + this.cacheMetadata.delete(key); + }); + + if (keysToDelete.length > 0) { + console.log('🧹 清理缓存:', keysToDelete.length, '项'); + this.saveCacheToStorage(); + } + } + + // 启动缓存清理定时器 + startCacheCleanup() { + setInterval(() => { + this.cleanupCache(); + }, 5 * 60 * 1000); // 每5分钟清理一次 + } + + // 📱 ===== 离线支持 ===== + + // 添加到离线队列 + addToOfflineQueue(request) { + if (!this.config.offline.enabled) return; + + // 检查离线数据大小 + const currentSize = this.getOfflineQueueSize(); + if (currentSize > this.config.offline.maxOfflineData) { + // 删除最旧的请求 + this.offlineQueue.shift(); + } + + this.offlineQueue.push({ + ...request, + timestamp: Date.now(), + retryCount: 0 + }); + + console.log('📱 添加到离线队列:', request.url); + this.stats.offlineRequests++; + } + + // 处理离线队列 + async processOfflineQueue() { + if (!this.networkState.isOnline || this.offlineQueue.length === 0) return; + + console.log('📱 处理离线队列:', this.offlineQueue.length, '个请求'); + + const queue = [...this.offlineQueue]; + this.offlineQueue = []; + + for (const request of queue) { + try { + await this.makeRequest(request); + console.log('✅ 离线请求同步成功:', request.url); + } catch (error) { + console.error('❌ 离线请求同步失败:', request.url, error); + + // 重试次数未达到上限,重新加入队列 + if (request.retryCount < this.config.request.retryAttempts) { + request.retryCount++; + this.offlineQueue.push(request); + } + } + } + } + + // 获取离线队列大小 + getOfflineQueueSize() { + return this.offlineQueue.reduce((size, request) => { + return size + this.getDataSize(request); + }, 0); + } + + // 🚀 ===== 请求优化 ===== + + // 优化的请求方法 + async optimizedRequest(options) { + const requestId = performanceMonitor.startApiMonitoring(options.url, options.method); + + try { + this.stats.totalRequests++; + + // 生成缓存键 + const cacheKey = this.generateCacheKey(options); + + // 检查缓存 + if (options.method === 'GET' || options.useCache) { + const cached = this.getCache(cacheKey); + if (cached) { + performanceMonitor.endApiMonitoring(requestId, { + success: true, + statusCode: 200, + fromCache: true + }); + + return this.config.cache.compressionEnabled ? + this.decompressData(cached) : cached; + } + } + + // 检查网络状态 + if (!this.networkState.isOnline) { + // 离线状态,添加到离线队列 + if (options.method !== 'GET') { + this.addToOfflineQueue(options); + } + + throw new Error('网络不可用,请求已加入离线队列'); + } + + // 检查并发限制 + if (this.activeRequests.size >= this.config.request.maxConcurrent) { + await this.waitForRequestSlot(); + } + + // 发起请求 + const result = await this.makeRequest(options); + + // 缓存GET请求结果 + if (options.method === 'GET' || options.useCache) { + this.setCache(cacheKey, result, options.cacheTTL); + } + + performanceMonitor.endApiMonitoring(requestId, { + success: true, + statusCode: result.statusCode || 200 + }); + + return result; + + } catch (error) { + this.stats.failedRequests++; + + performanceMonitor.endApiMonitoring(requestId, { + success: false, + errorMessage: error.message + }); + + // 使用错误处理器处理错误 + throw await errorHandler.handleError(error, { + url: options.url, + method: options.method + }); + } + } + + // 发起实际请求 + async makeRequest(options) { + const requestPromise = new Promise((resolve, reject) => { + const requestOptions = { + url: options.url, + method: options.method || 'GET', + data: options.data, + header: { + 'Content-Type': 'application/json', + ...options.header + }, + timeout: options.timeout || this.config.request.timeout, + success: (res) => { + this.activeRequests.delete(requestPromise); + resolve(res.data); + }, + fail: (error) => { + this.activeRequests.delete(requestPromise); + reject(new Error(error.errMsg || '请求失败')); + } + }; + + // 添加到活跃请求集合 + this.activeRequests.add(requestPromise); + + wx.request(requestOptions); + }); + + return requestPromise; + } + + // 等待请求槽位 + async waitForRequestSlot() { + while (this.activeRequests.size >= this.config.request.maxConcurrent) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + + // 启动请求队列处理器 + startRequestQueueProcessor() { + setInterval(() => { + this.processRequestQueue(); + }, 1000); + } + + // 处理请求队列 + processRequestQueue() { + while (this.requestQueue.length > 0 && + this.activeRequests.size < this.config.request.maxConcurrent) { + const request = this.requestQueue.shift(); + this.optimizedRequest(request.options) + .then(request.resolve) + .catch(request.reject); + } + } + + // 🚀 ===== 预加载 ===== + + // 添加预加载请求 + addPreloadRequest(url, options = {}) { + if (!this.config.preload.enabled) return; + + this.preloadQueue.push({ + url: url, + options: options, + priority: options.priority || 'normal', + timestamp: Date.now() + }); + + console.log('🚀 添加预加载请求:', url); + } + + // 启动预加载处理器 + startPreloadProcessor() { + setInterval(() => { + this.processPreloadQueue(); + }, this.config.preload.preloadDelay); + } + + // 处理预加载队列 + async processPreloadQueue() { + if (!this.networkState.isOnline || this.preloadQueue.length === 0) return; + + // 按优先级排序 + this.preloadQueue.sort((a, b) => { + const priorityOrder = { high: 3, normal: 2, low: 1 }; + return priorityOrder[b.priority] - priorityOrder[a.priority]; + }); + + // 处理高优先级的预加载请求 + const request = this.preloadQueue.shift(); + + try { + await this.optimizedRequest({ + url: request.url, + method: 'GET', + useCache: true, + ...request.options + }); + + console.log('🚀 预加载完成:', request.url); + + } catch (error) { + console.error('❌ 预加载失败:', request.url, error); + } + } + + // 🔧 ===== 工具方法 ===== + + // 生成缓存键 + generateCacheKey(options) { + const keyData = { + url: options.url, + method: options.method || 'GET', + data: options.data || {} + }; + + return `cache_${this.hashCode(JSON.stringify(keyData))}`; + } + + // 简单哈希函数 + hashCode(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // 转换为32位整数 + } + return Math.abs(hash).toString(36); + } + + // 压缩数据 + compressData(data) { + try { + // 简单的JSON压缩(移除空格) + return JSON.stringify(data); + } catch (error) { + return data; + } + } + + // 解压数据 + decompressData(data) { + try { + return typeof data === 'string' ? JSON.parse(data) : data; + } catch (error) { + return data; + } + } + + // 获取数据大小 + getDataSize(data) { + try { + return new Blob([JSON.stringify(data)]).size; + } catch (error) { + return JSON.stringify(data).length * 2; // 估算 + } + } + + // 保存缓存到本地存储 + async saveCacheToStorage() { + try { + // 将Map转换为对象进行存储 + const cacheData = Object.fromEntries(this.cache); + const metadataData = Object.fromEntries(this.cacheMetadata); + + wx.setStorageSync('network_cache', cacheData); + wx.setStorageSync('cache_metadata', metadataData); + + } catch (error) { + console.error('❌ 保存缓存到本地存储失败:', error); + } + } + + // 从本地存储加载缓存 + async loadCacheFromStorage() { + try { + const cacheData = wx.getStorageSync('network_cache'); + const metadataData = wx.getStorageSync('cache_metadata'); + + if (cacheData) { + this.cache = new Map(Object.entries(cacheData)); + } + + if (metadataData) { + this.cacheMetadata = new Map(Object.entries(metadataData)); + } + + console.log('📦 从本地存储加载缓存:', this.cache.size, '项'); + + } catch (error) { + console.error('❌ 从本地存储加载缓存失败:', error); + } + } + + // 获取网络统计 + getNetworkStats() { + this.stats.cacheHitRate = this.stats.totalRequests > 0 ? + this.stats.cachedRequests / this.stats.totalRequests : 0; + + return { + ...this.stats, + networkState: this.networkState, + cacheSize: this.getCacheSize(), + offlineQueueSize: this.offlineQueue.length, + activeRequests: this.activeRequests.size + }; + } + + // 销毁网络优化器 + destroy() { + // 保存缓存 + this.saveCacheToStorage(); + + // 清理数据 + this.cache.clear(); + this.cacheMetadata.clear(); + this.offlineQueue = []; + this.requestQueue = []; + this.activeRequests.clear(); + this.preloadQueue = []; + + this.isInitialized = false; + console.log('🌐 网络优化器已销毁'); + } +} + +// 创建全局实例 +const networkOptimizer = new NetworkOptimizer(); + +module.exports = networkOptimizer; diff --git a/utils/notification-manager.js b/utils/notification-manager.js new file mode 100644 index 0000000..8c19476 --- /dev/null +++ b/utils/notification-manager.js @@ -0,0 +1,592 @@ +// 实时通知管理器 - 微信小程序专用 +// 处理WebSocket消息、本地通知、订阅消息等 + +const wsManager = require('./websocket-manager-v2.js'); + +/** + * 微信小程序实时通知管理器 + * 功能: + * 1. WebSocket消息处理和分发 + * 2. 本地通知提醒 + * 3. 订阅消息管理 + * 4. 消息状态同步 + * 5. 离线消息处理 + */ +class NotificationManager { + constructor() { + this.isInitialized = false; + this.messageHandlers = new Map(); + this.notificationQueue = []; + this.unreadCounts = { + messages: 0, + friends: 0, + system: 0 + }; + + // 订阅消息模板ID(需要在微信公众平台配置) + this.subscribeTemplates = { + newMessage: 'template_id_for_new_message', + friendRequest: 'template_id_for_friend_request', + systemNotice: 'template_id_for_system_notice' + }; + + // 通知设置 + this.notificationSettings = { + sound: true, + vibrate: true, + showBadge: true, + quietHours: { + enabled: false, + start: '22:00', + end: '08:00' + } + }; + + this.init(); + } + + // 初始化通知管理器 + async init() { + if (this.isInitialized) return; + + console.log('🔔 初始化通知管理器...'); + + try { + // 加载通知设置 + await this.loadNotificationSettings(); + + // 注册WebSocket消息处理器 + this.registerWebSocketHandlers(); + + // 注册小程序生命周期事件 + this.registerAppLifecycleEvents(); + + // 初始化未读计数 + await this.loadUnreadCounts(); + + this.isInitialized = true; + console.log('✅ 通知管理器初始化完成'); + + } catch (error) { + console.error('❌ 通知管理器初始化失败:', error); + } + } + + // 注册WebSocket消息处理器 + registerWebSocketHandlers() { + // 新消息通知 + wsManager.on('message', (data) => { + this.handleWebSocketMessage(data); + }); + + // 连接状态变化 + wsManager.on('connected', () => { + console.log('🔔 WebSocket连接成功,开始接收通知'); + this.syncOfflineMessages(); + }); + + wsManager.on('disconnected', () => { + console.log('🔔 WebSocket连接断开,切换到离线模式'); + }); + } + + // 处理WebSocket消息 + async handleWebSocketMessage(data) { + try { + const message = typeof data === 'string' ? JSON.parse(data) : data; + console.log('📨 收到WebSocket消息:', message.type); + + switch (message.type) { + case 'new_message': + await this.handleNewMessage(message.data); + break; + case 'friend_request': + await this.handleFriendRequest(message.data); + break; + case 'friend_accepted': + await this.handleFriendAccepted(message.data); + break; + case 'system_notice': + await this.handleSystemNotice(message.data); + break; + case 'message_read': + await this.handleMessageRead(message.data); + break; + case 'user_online': + await this.handleUserOnline(message.data); + break; + case 'user_offline': + await this.handleUserOffline(message.data); + break; + default: + console.log('🔔 未知消息类型:', message.type); + } + + // 触发自定义事件 + this.triggerEvent('message_received', message); + + } catch (error) { + console.error('❌ 处理WebSocket消息失败:', error); + } + } + + // 处理新消息 + async handleNewMessage(messageData) { + console.log('💬 收到新消息:', messageData); + + // 更新未读计数 + this.unreadCounts.messages++; + await this.saveUnreadCounts(); + + // 显示通知 + await this.showNotification({ + type: 'new_message', + title: messageData.senderName || '新消息', + content: this.formatMessageContent(messageData), + data: messageData + }); + + // 触发页面更新 + this.triggerEvent('new_message', messageData); + + // 更新tabBar徽章 + this.updateTabBarBadge(); + } + + // 处理好友请求 + async handleFriendRequest(requestData) { + console.log('👥 收到好友请求:', requestData); + + // 更新未读计数 + this.unreadCounts.friends++; + await this.saveUnreadCounts(); + + // 显示通知 + await this.showNotification({ + type: 'friend_request', + title: '新的好友请求', + content: `${requestData.senderName} 请求添加您为好友`, + data: requestData + }); + + // 触发页面更新 + this.triggerEvent('friend_request', requestData); + + // 更新tabBar徽章 + this.updateTabBarBadge(); + } + + // 处理好友请求被接受 + async handleFriendAccepted(acceptData) { + console.log('✅ 好友请求被接受:', acceptData); + + // 显示通知 + await this.showNotification({ + type: 'friend_accepted', + title: '好友请求已接受', + content: `${acceptData.friendName} 已接受您的好友请求`, + data: acceptData + }); + + // 触发页面更新 + this.triggerEvent('friend_accepted', acceptData); + } + + // 处理系统通知 + async handleSystemNotice(noticeData) { + console.log('📢 收到系统通知:', noticeData); + + // 更新未读计数 + this.unreadCounts.system++; + await this.saveUnreadCounts(); + + // 显示通知 + await this.showNotification({ + type: 'system_notice', + title: '系统通知', + content: noticeData.content, + data: noticeData + }); + + // 触发页面更新 + this.triggerEvent('system_notice', noticeData); + } + + // 处理消息已读 + async handleMessageRead(readData) { + console.log('👁️ 消息已读:', readData); + + // 更新未读计数 + if (readData.count && this.unreadCounts.messages >= readData.count) { + this.unreadCounts.messages -= readData.count; + await this.saveUnreadCounts(); + this.updateTabBarBadge(); + } + + // 触发页面更新 + this.triggerEvent('message_read', readData); + } + + // 显示通知 + async showNotification(notification) { + try { + // 检查是否在静默时间 + if (this.isInQuietHours()) { + console.log('🔇 当前为静默时间,跳过通知'); + return; + } + + // 检查应用状态 + const appState = this.getAppState(); + + if (appState === 'foreground') { + // 前台显示本地通知 + await this.showLocalNotification(notification); + } else { + // 后台尝试发送订阅消息 + await this.sendSubscribeMessage(notification); + } + + // 播放提示音 + if (this.notificationSettings.sound) { + this.playNotificationSound(); + } + + // 震动提醒 + if (this.notificationSettings.vibrate) { + this.vibrateDevice(); + } + + } catch (error) { + console.error('❌ 显示通知失败:', error); + } + } + + // 显示本地通知(前台) + async showLocalNotification(notification) { + // 微信小程序没有原生的本地通知API + // 这里使用自定义的通知组件或Toast + + wx.showToast({ + title: notification.content, + icon: 'none', + duration: 3000 + }); + + // 如果有自定义通知组件,可以在这里调用 + // this.triggerEvent('show_custom_notification', notification); + } + + // 发送订阅消息(后台) + async sendSubscribeMessage(notification) { + try { + // 检查是否有订阅权限 + const templateId = this.getTemplateId(notification.type); + if (!templateId) { + console.log('🔔 没有对应的订阅消息模板'); + return; + } + + // 这里需要调用后端API发送订阅消息 + // 因为订阅消息需要在服务端发送 + console.log('📤 请求发送订阅消息:', { + templateId, + notification + }); + + // TODO: 调用后端API发送订阅消息 + + } catch (error) { + console.error('❌ 发送订阅消息失败:', error); + } + } + + // 格式化消息内容 + formatMessageContent(messageData) { + switch (messageData.msgType) { + case 0: // 文本消息 + return messageData.content; + case 1: // 图片消息 + return '[图片]'; + case 2: // 语音消息 + return '[语音]'; + case 3: // 视频消息 + return '[视频]'; + case 4: // 文件消息 + return '[文件]'; + default: + return '[消息]'; + } + } + + // 获取模板ID + getTemplateId(notificationType) { + return this.subscribeTemplates[notificationType]; + } + + // 检查是否在静默时间 + isInQuietHours() { + if (!this.notificationSettings.quietHours.enabled) { + return false; + } + + const now = new Date(); + const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`; + const { start, end } = this.notificationSettings.quietHours; + + if (start <= end) { + return currentTime >= start && currentTime <= end; + } else { + return currentTime >= start || currentTime <= end; + } + } + + // 获取应用状态 + getAppState() { + // 微信小程序中可以通过页面栈判断应用状态 + const pages = getCurrentPages(); + return pages.length > 0 ? 'foreground' : 'background'; + } + + // 播放提示音 + playNotificationSound() { + try { + // 微信小程序播放系统提示音 + wx.createInnerAudioContext().play(); + } catch (error) { + console.error('❌ 播放提示音失败:', error); + } + } + + // 震动设备 + vibrateDevice() { + try { + wx.vibrateShort({ + type: 'medium' + }); + } catch (error) { + console.error('❌ 震动失败:', error); + } + } + + // 更新tabBar徽章 + updateTabBarBadge() { + try { + const totalUnread = this.unreadCounts.messages + this.unreadCounts.friends + this.unreadCounts.system; + + if (totalUnread > 0) { + wx.setTabBarBadge({ + index: 1, // 消息页面的索引 + text: totalUnread > 99 ? '99+' : totalUnread.toString() + }); + } else { + wx.removeTabBarBadge({ + index: 1 + }); + } + } catch (error) { + console.error('❌ 更新tabBar徽章失败:', error); + } + } + + // 同步离线消息 + async syncOfflineMessages() { + try { + console.log('🔄 同步离线消息...'); + + // 获取最后同步时间 + const lastSyncTime = wx.getStorageSync('lastMessageSyncTime') || 0; + + // 调用API获取离线消息 + const apiClient = require('./api-client.js'); + const response = await apiClient.request({ + url: '/api/v1/messages/offline', + method: 'GET', + data: { + since: lastSyncTime + } + }); + + if (response.success && response.data.messages) { + console.log(`📥 收到 ${response.data.messages.length} 条离线消息`); + + // 处理离线消息 + for (const message of response.data.messages) { + await this.handleNewMessage(message); + } + + // 更新同步时间 + wx.setStorageSync('lastMessageSyncTime', Date.now()); + } + + } catch (error) { + console.error('❌ 同步离线消息失败:', error); + } + } + + // 注册小程序生命周期事件 + registerAppLifecycleEvents() { + // 监听小程序显示 + wx.onAppShow(() => { + console.log('🔔 小程序显示,检查未读消息'); + this.syncOfflineMessages(); + }); + + // 监听小程序隐藏 + wx.onAppHide(() => { + console.log('🔔 小程序隐藏,保存状态'); + this.saveUnreadCounts(); + }); + } + + // 加载通知设置 + async loadNotificationSettings() { + try { + const settings = wx.getStorageSync('notificationSettings'); + if (settings) { + this.notificationSettings = { ...this.notificationSettings, ...settings }; + } + } catch (error) { + console.error('❌ 加载通知设置失败:', error); + } + } + + // 保存通知设置 + async saveNotificationSettings() { + try { + wx.setStorageSync('notificationSettings', this.notificationSettings); + } catch (error) { + console.error('❌ 保存通知设置失败:', error); + } + } + + // 加载未读计数 + async loadUnreadCounts() { + try { + const counts = wx.getStorageSync('unreadCounts'); + if (counts) { + this.unreadCounts = { ...this.unreadCounts, ...counts }; + this.updateTabBarBadge(); + } + } catch (error) { + console.error('❌ 加载未读计数失败:', error); + } + } + + // 保存未读计数 + async saveUnreadCounts() { + try { + wx.setStorageSync('unreadCounts', this.unreadCounts); + } catch (error) { + console.error('❌ 保存未读计数失败:', error); + } + } + + // 清除未读计数 + async clearUnreadCount(type) { + if (this.unreadCounts[type] !== undefined) { + this.unreadCounts[type] = 0; + await this.saveUnreadCounts(); + this.updateTabBarBadge(); + } + } + + // 获取未读计数 + getUnreadCount(type) { + return this.unreadCounts[type] || 0; + } + + // 获取总未读计数 + getTotalUnreadCount() { + return Object.values(this.unreadCounts).reduce((total, count) => total + count, 0); + } + + // 更新通知设置 + updateNotificationSettings(settings) { + this.notificationSettings = { ...this.notificationSettings, ...settings }; + this.saveNotificationSettings(); + } + + // 请求订阅消息权限 + async requestSubscribeMessage(templateIds) { + try { + const result = await new Promise((resolve, reject) => { + wx.requestSubscribeMessage({ + tmplIds: Array.isArray(templateIds) ? templateIds : [templateIds], + success: resolve, + fail: reject + }); + }); + + console.log('📝 订阅消息权限请求结果:', result); + return result; + + } catch (error) { + console.error('❌ 请求订阅消息权限失败:', error); + return null; + } + } + + // 事件处理器 + eventHandlers = new Map(); + + // 注册事件监听器 + on(event, handler) { + if (!this.eventHandlers.has(event)) { + this.eventHandlers.set(event, []); + } + this.eventHandlers.get(event).push(handler); + } + + // 移除事件监听器 + off(event, handler) { + const handlers = this.eventHandlers.get(event); + if (handlers) { + const index = handlers.indexOf(handler); + if (index > -1) { + handlers.splice(index, 1); + } + } + } + + // 触发事件 + triggerEvent(event, data) { + const handlers = this.eventHandlers.get(event); + if (handlers) { + handlers.forEach(handler => { + try { + handler(data); + } catch (error) { + console.error(`❌ 事件处理器错误 [${event}]:`, error); + } + }); + } + } + + // 获取通知管理器状态 + getStatus() { + return { + isInitialized: this.isInitialized, + unreadCounts: { ...this.unreadCounts }, + notificationSettings: { ...this.notificationSettings }, + totalUnread: this.getTotalUnreadCount() + }; + } + + // 重置通知管理器 + reset() { + this.unreadCounts = { + messages: 0, + friends: 0, + system: 0 + }; + this.saveUnreadCounts(); + this.updateTabBarBadge(); + this.eventHandlers.clear(); + } +} + +// 创建全局实例 +const notificationManager = new NotificationManager(); + +module.exports = notificationManager; diff --git a/utils/performance-monitor.js b/utils/performance-monitor.js new file mode 100644 index 0000000..cbccd7e --- /dev/null +++ b/utils/performance-monitor.js @@ -0,0 +1,768 @@ +// 性能监控管理器 - 微信小程序专用 +// 监控应用性能、内存使用、网络请求等关键指标 + +/** + * 性能监控管理器 + * 功能: + * 1. 页面性能监控 + * 2. 内存使用监控 + * 3. 网络请求监控 + * 4. 错误监控和上报 + * 5. 性能数据分析 + * 6. 性能优化建议 + */ +class PerformanceMonitor { + constructor() { + this.isInitialized = false; + + // 性能配置 + this.config = { + // 监控开关 + enabled: true, + + // 采样率 (0-1) + sampleRate: 0.1, + + // 性能阈值 + thresholds: { + pageLoadTime: 3000, // 页面加载时间阈值 (ms) + apiResponseTime: 5000, // API响应时间阈值 (ms) + memoryUsage: 100, // 内存使用阈值 (MB) + errorRate: 0.05, // 错误率阈值 (5%) + crashRate: 0.01 // 崩溃率阈值 (1%) + }, + + // 上报配置 + reporting: { + enabled: true, + endpoint: '/api/v1/performance/report', + batchSize: 10, + interval: 30000 // 30秒上报一次 + } + }; + + // 性能数据 + this.performanceData = { + pageMetrics: new Map(), // 页面性能指标 + apiMetrics: new Map(), // API性能指标 + errorMetrics: new Map(), // 错误指标 + memoryMetrics: [], // 内存使用指标 + userMetrics: new Map() // 用户行为指标 + }; + + // 监控状态 + this.monitoringState = { + startTime: Date.now(), + sessionId: this.generateSessionId(), + userId: null, + deviceInfo: null, + networkType: 'unknown' + }; + + // 待上报数据 + this.pendingReports = []; + + // 定时器 + this.reportTimer = null; + this.memoryTimer = null; + + this.init(); + } + + // 初始化性能监控 + async init() { + if (this.isInitialized || !this.config.enabled) return; + + console.log('⚡ 初始化性能监控...'); + + try { + // 获取设备信息 + await this.getDeviceInfo(); + + // 获取网络类型 + await this.getNetworkType(); + + // 获取用户ID + this.monitoringState.userId = wx.getStorageSync('userId') || 'anonymous'; + + // 启动内存监控 + this.startMemoryMonitoring(); + + // 启动上报定时器 + this.startReportTimer(); + + // 监听应用生命周期 + this.setupAppLifecycleListeners(); + + // 监听网络状态变化 + this.setupNetworkListeners(); + + this.isInitialized = true; + console.log('✅ 性能监控初始化完成'); + + } catch (error) { + console.error('❌ 性能监控初始化失败:', error); + } + } + + // ⚡ ===== 页面性能监控 ===== + + // 开始页面性能监控 + startPageMonitoring(pagePath) { + if (!this.config.enabled) return; + + const pageId = this.generatePageId(pagePath); + const startTime = Date.now(); + + this.performanceData.pageMetrics.set(pageId, { + pagePath: pagePath, + startTime: startTime, + loadTime: null, + renderTime: null, + interactiveTime: null, + memoryUsage: null, + errors: [], + userActions: [] + }); + + console.log('⚡ 开始页面性能监控:', pagePath); + + return pageId; + } + + // 结束页面性能监控 + endPageMonitoring(pageId, metrics = {}) { + if (!this.config.enabled || !this.performanceData.pageMetrics.has(pageId)) return; + + const pageMetric = this.performanceData.pageMetrics.get(pageId); + const endTime = Date.now(); + + // 更新性能指标 + pageMetric.loadTime = endTime - pageMetric.startTime; + pageMetric.renderTime = metrics.renderTime || null; + pageMetric.interactiveTime = metrics.interactiveTime || null; + pageMetric.memoryUsage = this.getCurrentMemoryUsage(); + + // 检查性能阈值 + this.checkPagePerformance(pageMetric); + + // 添加到待上报数据 + this.addToReport('page_performance', pageMetric); + + console.log('⚡ 页面性能监控结束:', pageMetric.pagePath, `${pageMetric.loadTime}ms`); + } + + // 记录用户操作 + recordUserAction(pageId, action, data = {}) { + if (!this.config.enabled || !this.performanceData.pageMetrics.has(pageId)) return; + + const pageMetric = this.performanceData.pageMetrics.get(pageId); + pageMetric.userActions.push({ + action: action, + timestamp: Date.now(), + data: data + }); + } + + // 🌐 ===== API性能监控 ===== + + // 开始API请求监控 + startApiMonitoring(url, method = 'GET') { + if (!this.config.enabled) return null; + + const requestId = this.generateRequestId(); + const startTime = Date.now(); + + this.performanceData.apiMetrics.set(requestId, { + url: url, + method: method, + startTime: startTime, + endTime: null, + responseTime: null, + statusCode: null, + success: null, + errorMessage: null, + requestSize: null, + responseSize: null + }); + + return requestId; + } + + // 结束API请求监控 + endApiMonitoring(requestId, result = {}) { + if (!this.config.enabled || !this.performanceData.apiMetrics.has(requestId)) return; + + const apiMetric = this.performanceData.apiMetrics.get(requestId); + const endTime = Date.now(); + + // 更新API指标 + apiMetric.endTime = endTime; + apiMetric.responseTime = endTime - apiMetric.startTime; + apiMetric.statusCode = result.statusCode || null; + apiMetric.success = result.success || false; + apiMetric.errorMessage = result.errorMessage || null; + apiMetric.requestSize = result.requestSize || null; + apiMetric.responseSize = result.responseSize || null; + + // 检查API性能 + this.checkApiPerformance(apiMetric); + + // 添加到待上报数据 + this.addToReport('api_performance', apiMetric); + + console.log('⚡ API性能监控:', apiMetric.url, `${apiMetric.responseTime}ms`); + } + + // 📊 ===== 内存监控 ===== + + // 启动内存监控 + startMemoryMonitoring() { + if (!this.config.enabled) return; + + this.memoryTimer = setInterval(() => { + this.collectMemoryMetrics(); + }, 10000); // 每10秒收集一次内存数据 + } + + // 收集内存指标 + collectMemoryMetrics() { + try { + // 使用新的API替代已弃用的wx.getSystemInfoSync + const deviceInfo = wx.getDeviceInfo(); + const memoryInfo = { system: deviceInfo.memorySize || 'unknown' }; + const currentMemory = this.getCurrentMemoryUsage(); + + const memoryMetric = { + timestamp: Date.now(), + totalMemory: memoryInfo.system || 'unknown', + usedMemory: currentMemory, + availableMemory: memoryInfo.system ? (memoryInfo.system - currentMemory) : 'unknown', + memoryWarning: currentMemory > this.config.thresholds.memoryUsage + }; + + this.performanceData.memoryMetrics.push(memoryMetric); + + // 保持最近100条记录 + if (this.performanceData.memoryMetrics.length > 100) { + this.performanceData.memoryMetrics.shift(); + } + + // 检查内存使用 + if (memoryMetric.memoryWarning) { + this.handleMemoryWarning(memoryMetric); + } + + } catch (error) { + console.error('❌ 收集内存指标失败:', error); + } + } + + // 获取当前内存使用 + getCurrentMemoryUsage() { + try { + // 微信小程序没有直接的内存API,使用估算方法 + const pages = getCurrentPages(); + const cacheSize = this.estimateCacheSize(); + + // 估算内存使用 (页面数 * 5MB + 缓存大小) + return pages.length * 5 + cacheSize; + } catch (error) { + return 0; + } + } + + // 估算缓存大小 + estimateCacheSize() { + try { + const storageInfo = wx.getStorageInfoSync(); + return Math.round(storageInfo.currentSize / 1024); // 转换为MB + } catch (error) { + return 0; + } + } + + // 🚨 ===== 错误监控 ===== + + // 记录错误 + recordError(error, context = {}) { + if (!this.config.enabled) return; + + const errorId = this.generateErrorId(); + const errorMetric = { + errorId: errorId, + timestamp: Date.now(), + message: error.message || error.toString(), + stack: error.stack || null, + type: error.name || 'UnknownError', + context: context, + userId: this.monitoringState.userId, + sessionId: this.monitoringState.sessionId, + pagePath: this.getCurrentPagePath(), + deviceInfo: this.monitoringState.deviceInfo, + networkType: this.monitoringState.networkType + }; + + this.performanceData.errorMetrics.set(errorId, errorMetric); + + // 添加到待上报数据 + this.addToReport('error', errorMetric); + + console.error('🚨 错误记录:', errorMetric); + } + + // 记录崩溃 + recordCrash(crashInfo) { + if (!this.config.enabled) return; + + const crashMetric = { + timestamp: Date.now(), + crashInfo: crashInfo, + userId: this.monitoringState.userId, + sessionId: this.monitoringState.sessionId, + deviceInfo: this.monitoringState.deviceInfo, + memoryUsage: this.getCurrentMemoryUsage(), + recentErrors: Array.from(this.performanceData.errorMetrics.values()).slice(-5) + }; + + // 立即上报崩溃数据 + this.reportImmediately('crash', crashMetric); + + console.error('💥 崩溃记录:', crashMetric); + } + + // 📈 ===== 性能分析 ===== + + // 检查页面性能 + checkPagePerformance(pageMetric) { + const warnings = []; + + if (pageMetric.loadTime > this.config.thresholds.pageLoadTime) { + warnings.push(`页面加载时间过长: ${pageMetric.loadTime}ms`); + } + + if (pageMetric.memoryUsage > this.config.thresholds.memoryUsage) { + warnings.push(`内存使用过高: ${pageMetric.memoryUsage}MB`); + } + + if (warnings.length > 0) { + console.warn('⚠️ 页面性能警告:', pageMetric.pagePath, warnings); + this.addToReport('performance_warning', { + type: 'page', + pagePath: pageMetric.pagePath, + warnings: warnings, + metrics: pageMetric + }); + } + } + + // 检查API性能 + checkApiPerformance(apiMetric) { + const warnings = []; + + if (apiMetric.responseTime > this.config.thresholds.apiResponseTime) { + warnings.push(`API响应时间过长: ${apiMetric.responseTime}ms`); + } + + if (!apiMetric.success) { + warnings.push(`API请求失败: ${apiMetric.errorMessage}`); + } + + if (warnings.length > 0) { + console.warn('⚠️ API性能警告:', apiMetric.url, warnings); + this.addToReport('performance_warning', { + type: 'api', + url: apiMetric.url, + warnings: warnings, + metrics: apiMetric + }); + } + } + + // 处理内存警告 + handleMemoryWarning(memoryMetric) { + console.warn('⚠️ 内存使用警告:', memoryMetric); + + // 触发内存清理 + this.triggerMemoryCleanup(); + + // 上报内存警告 + this.addToReport('memory_warning', memoryMetric); + } + + // 触发内存清理 + triggerMemoryCleanup() { + try { + // 清理过期缓存 + this.cleanupExpiredCache(); + + // 清理性能数据 + this.cleanupPerformanceData(); + + // 通知应用进行内存清理 + wx.triggerGC && wx.triggerGC(); + + console.log('🧹 内存清理完成'); + } catch (error) { + console.error('❌ 内存清理失败:', error); + } + } + + // 🔧 ===== 工具方法 ===== + + // 生成会话ID + generateSessionId() { + return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + // 生成页面ID + generatePageId(pagePath) { + return `page_${pagePath.replace(/\//g, '_')}_${Date.now()}`; + } + + // 生成请求ID + generateRequestId() { + return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + // 生成错误ID + generateErrorId() { + return `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + // 获取当前页面路径 + getCurrentPagePath() { + try { + const pages = getCurrentPages(); + const currentPage = pages[pages.length - 1]; + return currentPage ? currentPage.route : 'unknown'; + } catch (error) { + return 'unknown'; + } + } + + // 获取设备信息 + async getDeviceInfo() { + try { + // 使用新的API替代已弃用的wx.getSystemInfoSync + const deviceInfo = wx.getDeviceInfo(); + const windowInfo = wx.getWindowInfo(); + const appBaseInfo = wx.getAppBaseInfo(); + + this.monitoringState.deviceInfo = { + brand: deviceInfo.brand, + model: deviceInfo.model, + system: deviceInfo.system, + platform: deviceInfo.platform, + version: appBaseInfo.version, + SDKVersion: appBaseInfo.SDKVersion, + screenWidth: windowInfo.screenWidth, + screenHeight: windowInfo.screenHeight, + pixelRatio: windowInfo.pixelRatio + }; + } catch (error) { + console.error('❌ 获取设备信息失败,使用兜底方案:', error); + // 兜底方案 + try { + const systemInfo = wx.getSystemInfoSync(); + this.monitoringState.deviceInfo = { + brand: systemInfo.brand, + model: systemInfo.model, + system: systemInfo.system, + platform: systemInfo.platform, + version: systemInfo.version, + SDKVersion: systemInfo.SDKVersion, + screenWidth: systemInfo.screenWidth, + screenHeight: systemInfo.screenHeight, + pixelRatio: systemInfo.pixelRatio + }; + } catch (fallbackError) { + console.error('❌ 兜底方案也失败了:', fallbackError); + this.monitoringState.deviceInfo = { + brand: 'unknown', + model: 'unknown', + system: 'unknown', + platform: 'unknown', + version: 'unknown', + SDKVersion: 'unknown', + screenWidth: 375, + screenHeight: 667, + pixelRatio: 2 + }; + } + } + } + + // 获取网络类型 + async getNetworkType() { + try { + const networkInfo = await new Promise((resolve, reject) => { + wx.getNetworkType({ + success: resolve, + fail: reject + }); + }); + + this.monitoringState.networkType = networkInfo.networkType; + } catch (error) { + console.error('❌ 获取网络类型失败:', error); + } + } + + // 设置应用生命周期监听 + setupAppLifecycleListeners() { + // 监听应用隐藏 + wx.onAppHide(() => { + this.addToReport('app_lifecycle', { + event: 'hide', + timestamp: Date.now(), + sessionDuration: Date.now() - this.monitoringState.startTime + }); + }); + + // 监听应用显示 + wx.onAppShow(() => { + this.addToReport('app_lifecycle', { + event: 'show', + timestamp: Date.now() + }); + }); + } + + // 设置网络状态监听 + setupNetworkListeners() { + wx.onNetworkStatusChange((res) => { + this.monitoringState.networkType = res.networkType; + + this.addToReport('network_change', { + networkType: res.networkType, + isConnected: res.isConnected, + timestamp: Date.now() + }); + }); + } + + // 添加到上报队列 + addToReport(type, data) { + if (!this.config.reporting.enabled) return; + + // 采样控制 + if (Math.random() > this.config.sampleRate) return; + + this.pendingReports.push({ + type: type, + data: data, + timestamp: Date.now(), + sessionId: this.monitoringState.sessionId, + userId: this.monitoringState.userId + }); + + // 检查是否需要立即上报 + if (this.pendingReports.length >= this.config.reporting.batchSize) { + this.reportData(); + } + } + + // 立即上报 + async reportImmediately(type, data) { + if (!this.config.reporting.enabled) return; + + try { + const reportData = { + type: type, + data: data, + timestamp: Date.now(), + sessionId: this.monitoringState.sessionId, + userId: this.monitoringState.userId, + deviceInfo: this.monitoringState.deviceInfo + }; + + // 这里应该调用实际的上报API + console.log('📊 立即上报性能数据:', reportData); + + } catch (error) { + console.error('❌ 立即上报失败:', error); + } + } + + // 启动上报定时器 + startReportTimer() { + if (!this.config.reporting.enabled) return; + + this.reportTimer = setInterval(() => { + if (this.pendingReports.length > 0) { + this.reportData(); + } + }, this.config.reporting.interval); + } + + // 上报数据 + async reportData() { + if (!this.config.reporting.enabled || this.pendingReports.length === 0) return; + + try { + const reports = this.pendingReports.splice(0, this.config.reporting.batchSize); + + // 这里应该调用实际的上报API + console.log('📊 批量上报性能数据:', reports.length, '条'); + + // 模拟API调用 + // await apiClient.request({ + // url: this.config.reporting.endpoint, + // method: 'POST', + // data: { + // reports: reports, + // deviceInfo: this.monitoringState.deviceInfo, + // sessionInfo: { + // sessionId: this.monitoringState.sessionId, + // startTime: this.monitoringState.startTime, + // userId: this.monitoringState.userId + // } + // } + // }); + + } catch (error) { + console.error('❌ 上报性能数据失败:', error); + + // 上报失败,将数据重新加入队列 + // this.pendingReports.unshift(...reports); + } + } + + // 清理过期缓存 + cleanupExpiredCache() { + try { + // 清理过期的性能数据 + const now = Date.now(); + const expireTime = 24 * 60 * 60 * 1000; // 24小时 + + // 清理页面指标 + for (const [key, value] of this.performanceData.pageMetrics) { + if (now - value.startTime > expireTime) { + this.performanceData.pageMetrics.delete(key); + } + } + + // 清理API指标 + for (const [key, value] of this.performanceData.apiMetrics) { + if (now - value.startTime > expireTime) { + this.performanceData.apiMetrics.delete(key); + } + } + + // 清理错误指标 + for (const [key, value] of this.performanceData.errorMetrics) { + if (now - value.timestamp > expireTime) { + this.performanceData.errorMetrics.delete(key); + } + } + + } catch (error) { + console.error('❌ 清理过期缓存失败:', error); + } + } + + // 清理性能数据 + cleanupPerformanceData() { + try { + // 保留最近的数据 + const maxPageMetrics = 50; + const maxApiMetrics = 100; + const maxErrorMetrics = 50; + + // 清理页面指标 + if (this.performanceData.pageMetrics.size > maxPageMetrics) { + const entries = Array.from(this.performanceData.pageMetrics.entries()); + entries.sort((a, b) => b[1].startTime - a[1].startTime); + + this.performanceData.pageMetrics.clear(); + entries.slice(0, maxPageMetrics).forEach(([key, value]) => { + this.performanceData.pageMetrics.set(key, value); + }); + } + + // 清理API指标 + if (this.performanceData.apiMetrics.size > maxApiMetrics) { + const entries = Array.from(this.performanceData.apiMetrics.entries()); + entries.sort((a, b) => b[1].startTime - a[1].startTime); + + this.performanceData.apiMetrics.clear(); + entries.slice(0, maxApiMetrics).forEach(([key, value]) => { + this.performanceData.apiMetrics.set(key, value); + }); + } + + // 清理错误指标 + if (this.performanceData.errorMetrics.size > maxErrorMetrics) { + const entries = Array.from(this.performanceData.errorMetrics.entries()); + entries.sort((a, b) => b[1].timestamp - a[1].timestamp); + + this.performanceData.errorMetrics.clear(); + entries.slice(0, maxErrorMetrics).forEach(([key, value]) => { + this.performanceData.errorMetrics.set(key, value); + }); + } + + } catch (error) { + console.error('❌ 清理性能数据失败:', error); + } + } + + // 获取性能报告 + getPerformanceReport() { + return { + sessionInfo: this.monitoringState, + pageMetrics: Array.from(this.performanceData.pageMetrics.values()), + apiMetrics: Array.from(this.performanceData.apiMetrics.values()), + errorMetrics: Array.from(this.performanceData.errorMetrics.values()), + memoryMetrics: this.performanceData.memoryMetrics, + summary: this.generatePerformanceSummary() + }; + } + + // 生成性能摘要 + generatePerformanceSummary() { + const pageMetrics = Array.from(this.performanceData.pageMetrics.values()); + const apiMetrics = Array.from(this.performanceData.apiMetrics.values()); + const errorMetrics = Array.from(this.performanceData.errorMetrics.values()); + + return { + totalPages: pageMetrics.length, + averagePageLoadTime: pageMetrics.length > 0 ? + pageMetrics.reduce((sum, m) => sum + (m.loadTime || 0), 0) / pageMetrics.length : 0, + totalApiRequests: apiMetrics.length, + averageApiResponseTime: apiMetrics.length > 0 ? + apiMetrics.reduce((sum, m) => sum + (m.responseTime || 0), 0) / apiMetrics.length : 0, + totalErrors: errorMetrics.length, + errorRate: pageMetrics.length > 0 ? errorMetrics.length / pageMetrics.length : 0, + currentMemoryUsage: this.getCurrentMemoryUsage(), + sessionDuration: Date.now() - this.monitoringState.startTime + }; + } + + // 销毁监控器 + destroy() { + if (this.reportTimer) { + clearInterval(this.reportTimer); + this.reportTimer = null; + } + + if (this.memoryTimer) { + clearInterval(this.memoryTimer); + this.memoryTimer = null; + } + + // 最后一次上报 + if (this.pendingReports.length > 0) { + this.reportData(); + } + + this.isInitialized = false; + console.log('⚡ 性能监控器已销毁'); + } +} + +// 创建全局实例 +const performanceMonitor = new PerformanceMonitor(); + +module.exports = performanceMonitor; diff --git a/utils/performance-optimizer.js b/utils/performance-optimizer.js new file mode 100644 index 0000000..99f0e60 --- /dev/null +++ b/utils/performance-optimizer.js @@ -0,0 +1,325 @@ +// 性能优化工具 - 提升小程序性能和用户体验 +class PerformanceOptimizer { + constructor() { + this.imageCache = new Map(); + this.requestCache = new Map(); + this.lazyLoadObserver = null; + this.performanceMetrics = { + pageLoadTimes: [], + apiResponseTimes: [], + imageLoadTimes: [] + }; + } + + // 🔥 ===== 图片优化 ===== + + // 图片懒加载 + initLazyLoad() { + // 创建懒加载观察器 + this.lazyLoadObserver = wx.createIntersectionObserver(); + + this.lazyLoadObserver.observe('.lazy-image', (res) => { + if (res.intersectionRatio > 0) { + // 图片进入视口,开始加载 + const dataset = res.dataset; + if (dataset && dataset.src) { + this.loadImage(dataset.src, res.id); + } + } + }); + } + + // 优化图片加载 + async loadImage(src, elementId) { + try { + const startTime = Date.now(); + + // 检查缓存 + if (this.imageCache.has(src)) { + const cachedImage = this.imageCache.get(src); + this.updateImageElement(elementId, cachedImage); + return cachedImage; + } + + // 预加载图片 + const imageInfo = await this.preloadImage(src); + + // 缓存图片信息 + this.imageCache.set(src, imageInfo); + + // 更新元素 + this.updateImageElement(elementId, imageInfo); + + // 记录性能指标 + const loadTime = Date.now() - startTime; + this.recordImageLoadTime(loadTime); + + return imageInfo; + + } catch (error) { + console.error('图片加载失败:', error); + // 使用默认图片 + this.updateImageElement(elementId, { path: '/assets/images/placeholder.png' }); + } + } + + // 预加载图片 + preloadImage(src) { + return new Promise((resolve, reject) => { + wx.getImageInfo({ + src: src, + success: resolve, + fail: reject + }); + }); + } + + // 更新图片元素 + updateImageElement(elementId, imageInfo) { + // 这里需要页面配合实现具体的更新逻辑 + console.log('更新图片元素:', elementId, imageInfo); + } + + // 🔥 ===== 请求优化 ===== + + // 请求缓存 + async cacheRequest(key, requestFn, ttl = 300000) { // 默认5分钟缓存 + const now = Date.now(); + + // 检查缓存 + if (this.requestCache.has(key)) { + const cached = this.requestCache.get(key); + if (now - cached.timestamp < ttl) { + console.log('使用缓存数据:', key); + return cached.data; + } + } + + try { + const startTime = Date.now(); + const data = await requestFn(); + const responseTime = Date.now() - startTime; + + // 缓存数据 + this.requestCache.set(key, { + data: data, + timestamp: now + }); + + // 记录性能指标 + this.recordApiResponseTime(responseTime); + + return data; + + } catch (error) { + console.error('请求失败:', error); + throw error; + } + } + + // 清除过期缓存 + clearExpiredCache() { + const now = Date.now(); + const maxAge = 600000; // 10分钟 + + for (const [key, value] of this.requestCache.entries()) { + if (now - value.timestamp > maxAge) { + this.requestCache.delete(key); + } + } + } + + // 🔥 ===== 内存优化 ===== + + // 清理内存 + cleanupMemory() { + // 清理图片缓存 + if (this.imageCache.size > 50) { + const entries = Array.from(this.imageCache.entries()); + const toDelete = entries.slice(0, entries.length - 30); + toDelete.forEach(([key]) => { + this.imageCache.delete(key); + }); + } + + // 清理请求缓存 + this.clearExpiredCache(); + + // 触发垃圾回收(如果可用) + if (wx.triggerGC) { + wx.triggerGC(); + } + } + + // 监控内存使用 + monitorMemory() { + if (wx.getPerformance) { + const performance = wx.getPerformance(); + const memory = performance.memory; + + if (memory) { + console.log('内存使用情况:', { + used: this.formatBytes(memory.usedJSHeapSize), + total: this.formatBytes(memory.totalJSHeapSize), + limit: this.formatBytes(memory.jsHeapSizeLimit) + }); + + // 如果内存使用超过80%,触发清理 + if (memory.usedJSHeapSize / memory.jsHeapSizeLimit > 0.8) { + console.warn('内存使用过高,开始清理'); + this.cleanupMemory(); + } + } + } + } + + // 🔥 ===== 性能监控 ===== + + // 记录页面加载时间 + recordPageLoadTime(loadTime) { + this.performanceMetrics.pageLoadTimes.push(loadTime); + + // 只保留最近100条记录 + if (this.performanceMetrics.pageLoadTimes.length > 100) { + this.performanceMetrics.pageLoadTimes.shift(); + } + } + + // 记录API响应时间 + recordApiResponseTime(responseTime) { + this.performanceMetrics.apiResponseTimes.push(responseTime); + + if (this.performanceMetrics.apiResponseTimes.length > 100) { + this.performanceMetrics.apiResponseTimes.shift(); + } + } + + // 记录图片加载时间 + recordImageLoadTime(loadTime) { + this.performanceMetrics.imageLoadTimes.push(loadTime); + + if (this.performanceMetrics.imageLoadTimes.length > 100) { + this.performanceMetrics.imageLoadTimes.shift(); + } + } + + // 获取性能报告 + getPerformanceReport() { + const report = { + pageLoad: this.calculateStats(this.performanceMetrics.pageLoadTimes), + apiResponse: this.calculateStats(this.performanceMetrics.apiResponseTimes), + imageLoad: this.calculateStats(this.performanceMetrics.imageLoadTimes), + cacheStats: { + imageCache: this.imageCache.size, + requestCache: this.requestCache.size + } + }; + + console.log('性能报告:', report); + return report; + } + + // 计算统计数据 + calculateStats(times) { + if (times.length === 0) { + return { avg: 0, min: 0, max: 0, count: 0 }; + } + + const sum = times.reduce((a, b) => a + b, 0); + const avg = sum / times.length; + const min = Math.min(...times); + const max = Math.max(...times); + + return { + avg: Math.round(avg), + min: min, + max: max, + count: times.length + }; + } + + // 🔥 ===== 工具方法 ===== + + // 格式化字节数 + formatBytes(bytes) { + if (bytes === 0) return '0 B'; + + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + // 防抖函数 + debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } + + // 节流函数 + throttle(func, limit) { + let inThrottle; + return function executedFunction(...args) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; + } + + // 🔥 ===== 初始化和清理 ===== + + // 初始化性能优化 + init() { + console.log('初始化性能优化器'); + + // 初始化懒加载 + this.initLazyLoad(); + + // 定期清理内存 + setInterval(() => { + this.cleanupMemory(); + }, 300000); // 5分钟清理一次 + + // 定期监控内存 + setInterval(() => { + this.monitorMemory(); + }, 60000); // 1分钟监控一次 + } + + // 销毁优化器 + destroy() { + console.log('销毁性能优化器'); + + // 清理懒加载观察器 + if (this.lazyLoadObserver) { + this.lazyLoadObserver.disconnect(); + this.lazyLoadObserver = null; + } + + // 清理缓存 + this.imageCache.clear(); + this.requestCache.clear(); + + // 清理性能指标 + this.performanceMetrics = { + pageLoadTimes: [], + apiResponseTimes: [], + imageLoadTimes: [] + }; + } +} + +// 创建全局单例 +const performanceOptimizer = new PerformanceOptimizer(); + +module.exports = performanceOptimizer; diff --git a/utils/screen-adapter.js b/utils/screen-adapter.js new file mode 100644 index 0000000..50fa24f --- /dev/null +++ b/utils/screen-adapter.js @@ -0,0 +1,354 @@ +// 屏幕适配工具类 - 解决小程序滚动条问题 +// 基于2024年最新的微信小程序最佳实践 + +class ScreenAdapter { + constructor() { + this.systemInfo = null; + this.isInitialized = false; + this.deviceInfo = {}; + this.adaptationInfo = {}; + } + + // 初始化适配器 + async init() { + if (this.isInitialized) { + return this.adaptationInfo; + } + + try { + // 使用新的API获取系统信息 + const [windowInfo, deviceInfo, appBaseInfo] = await Promise.all([ + this.getWindowInfo(), + this.getDeviceInfo(), + this.getAppBaseInfo() + ]); + + // 合并系统信息 + this.systemInfo = { + ...windowInfo, + ...deviceInfo, + ...appBaseInfo + }; + + // 计算适配信息 + this.calculateAdaptation(); + + this.isInitialized = true; + console.log('屏幕适配器初始化成功:', this.adaptationInfo); + + return this.adaptationInfo; + } catch (error) { + console.error('屏幕适配器初始化失败:', error); + // 使用兜底方案 + this.fallbackInit(); + return this.adaptationInfo; + } + } + + // 获取窗口信息(新API) + getWindowInfo() { + return new Promise((resolve) => { + try { + const windowInfo = wx.getWindowInfo(); + resolve(windowInfo); + } catch (error) { + console.warn('getWindowInfo失败,使用兜底方案:', error); + resolve({ + windowWidth: 375, + windowHeight: 667, + pixelRatio: 2, + safeArea: { + top: 44, + bottom: 0, + left: 0, + right: 375 + } + }); + } + }); + } + + // 获取设备信息(新API) + getDeviceInfo() { + return new Promise((resolve) => { + try { + const deviceInfo = wx.getDeviceInfo(); + resolve(deviceInfo); + } catch (error) { + console.warn('getDeviceInfo失败,使用兜底方案:', error); + resolve({ + platform: 'unknown', + system: 'unknown', + model: 'unknown' + }); + } + }); + } + + // 获取应用基础信息(新API) + getAppBaseInfo() { + return new Promise((resolve) => { + try { + const appBaseInfo = wx.getAppBaseInfo(); + resolve(appBaseInfo); + } catch (error) { + console.warn('getAppBaseInfo失败,使用兜底方案:', error); + resolve({ + version: '1.0.0', + language: 'zh_CN' + }); + } + }); + } + + // 兜底初始化 + fallbackInit() { + try { + this.systemInfo = wx.getSystemInfoSync(); + this.calculateAdaptation(); + this.isInitialized = true; + } catch (error) { + console.error('兜底初始化也失败,使用硬编码默认值:', error); + this.useDefaultValues(); + } + } + + // 计算适配信息 + calculateAdaptation() { + const { windowWidth, windowHeight, safeArea, pixelRatio = 2 } = this.systemInfo; + + // 计算安全区域 + const statusBarHeight = safeArea ? safeArea.top : 44; + const safeAreaBottom = safeArea ? (windowHeight - safeArea.bottom) : 0; + + // 计算胶囊按钮信息(兜底处理) + let menuButtonInfo = { height: 32, top: 6, width: 87, right: 281 }; + try { + menuButtonInfo = wx.getMenuButtonBoundingClientRect(); + } catch (error) { + console.warn('获取胶囊按钮信息失败,使用默认值:', error); + } + + const menuButtonHeight = menuButtonInfo.height; + const menuButtonTop = menuButtonInfo.top - statusBarHeight; + const navBarHeight = statusBarHeight + menuButtonHeight + menuButtonTop * 2; + + // 计算可用高度(解决滚动条问题的关键) + const usableHeight = windowHeight; + const contentHeight = usableHeight - statusBarHeight - safeAreaBottom; + + // 设备类型判断 + const deviceType = this.detectDeviceType(windowWidth, windowHeight); + + this.adaptationInfo = { + // 基础信息 + windowWidth, + windowHeight, + pixelRatio, + + // 安全区域 + statusBarHeight, + safeAreaBottom, + safeAreaTop: statusBarHeight, + safeAreaLeft: safeArea ? safeArea.left : 0, + safeAreaRight: safeArea ? (windowWidth - safeArea.right) : 0, + + // 导航栏信息 + navBarHeight, + menuButtonHeight, + menuButtonTop, + menuButtonInfo, + + // 可用区域(解决滚动条的关键) + usableHeight, + contentHeight, + + // 设备信息 + deviceType, + platform: this.systemInfo.platform, + + // CSS变量(用于动态设置样式) + cssVars: { + '--window-height': windowHeight + 'px', + '--window-width': windowWidth + 'px', + '--status-bar-height': statusBarHeight + 'px', + '--nav-bar-height': navBarHeight + 'px', + '--safe-area-bottom': safeAreaBottom + 'px', + '--content-height': contentHeight + 'px', + '--usable-height': usableHeight + 'px' + } + }; + } + + // 检测设备类型 + detectDeviceType(width, height) { + // iPhone SE系列 + if (width <= 375 && height <= 667) { + return 'iphone-se'; + } + // iPhone 12/13/14系列 + else if (width <= 390 && height >= 844) { + return 'iphone-standard'; + } + // iPhone 12/13/14 Pro Max系列 + else if (width <= 428 && height >= 926) { + return 'iphone-max'; + } + // iPad系列 + else if (width >= 768) { + return 'ipad'; + } + // Android大屏 + else if (height >= 800) { + return 'android-large'; + } + // 其他设备 + else { + return 'unknown'; + } + } + + // 使用默认值 + useDefaultValues() { + this.adaptationInfo = { + windowWidth: 375, + windowHeight: 667, + pixelRatio: 2, + statusBarHeight: 44, + safeAreaBottom: 0, + safeAreaTop: 44, + safeAreaLeft: 0, + safeAreaRight: 0, + navBarHeight: 88, + menuButtonHeight: 32, + menuButtonTop: 6, + usableHeight: 667, + contentHeight: 623, + deviceType: 'unknown', + platform: 'unknown', + cssVars: { + '--window-height': '667px', + '--window-width': '375px', + '--status-bar-height': '44px', + '--nav-bar-height': '88px', + '--safe-area-bottom': '0px', + '--content-height': '623px', + '--usable-height': '667px' + } + }; + this.isInitialized = true; + } + + // 获取适配信息 + getAdaptationInfo() { + if (!this.isInitialized) { + console.warn('屏幕适配器未初始化,返回默认值'); + this.useDefaultValues(); + } + return this.adaptationInfo; + } + + // 为页面设置适配样式(解决滚动条的核心方法) + applyToPage(pageInstance) { + if (!pageInstance) { + console.error('页面实例为空'); + return; + } + + const info = this.getAdaptationInfo(); + + // 🔥 设置页面数据 - 使用Object.assign替代扩展运算符 + pageInstance.setData(Object.assign({}, info, { + // 兼容旧版本的字段名 + windowHeight: info.windowHeight, + windowWidth: info.windowWidth, + statusBarHeight: info.statusBarHeight, + navBarHeight: info.navBarHeight, + safeAreaBottom: info.safeAreaBottom, + menuButtonHeight: info.menuButtonHeight, + menuButtonTop: info.menuButtonTop + })); + + // 应用CSS变量到页面 + this.applyCSSVars(); + + console.log('页面适配应用成功:', pageInstance.route || 'unknown'); + } + + // 应用CSS变量 + applyCSSVars() { + if (!this.adaptationInfo.cssVars) return; + + try { + // 在小程序中,我们通过设置页面数据来传递这些值 + // CSS变量将通过页面数据在WXML中使用 + const pages = getCurrentPages(); + const currentPage = pages[pages.length - 1]; + if (currentPage) { + currentPage.setData({ + screenCSSVars: this.adaptationInfo.cssVars + }); + } + } catch (error) { + console.error('应用CSS变量失败:', error); + } + } + + // 监听窗口大小变化(横竖屏切换等) + onWindowResize(callback) { + wx.onWindowResize((res) => { + console.log('窗口大小变化:', res); + // 重新计算适配信息 + this.systemInfo.windowWidth = res.size.windowWidth; + this.systemInfo.windowHeight = res.size.windowHeight; + this.calculateAdaptation(); + + if (callback && typeof callback === 'function') { + callback(this.adaptationInfo); + } + }); + } + + // 获取推荐的页面配置 + getPageConfig() { + return { + // 禁用页面滚动,防止出现滚动条 + disableScroll: true, + // 背景色 + backgroundColor: '#f8f9fa', + // 导航栏样式 + navigationBarBackgroundColor: '#667eea', + navigationBarTextStyle: 'white' + }; + } +} + +// 创建全局实例 +const screenAdapter = new ScreenAdapter(); + +// 导出实例和类 +module.exports = { + ScreenAdapter, + screenAdapter, + + // 便捷方法 + async init() { + return await screenAdapter.init(); + }, + + getAdaptationInfo() { + return screenAdapter.getAdaptationInfo(); + }, + + applyToPage(pageInstance) { + return screenAdapter.applyToPage(pageInstance); + }, + + onWindowResize(callback) { + return screenAdapter.onWindowResize(callback); + }, + + getPageConfig() { + return screenAdapter.getPageConfig(); + } +}; \ No newline at end of file diff --git a/utils/storage.js b/utils/storage.js new file mode 100644 index 0000000..524d52c --- /dev/null +++ b/utils/storage.js @@ -0,0 +1,294 @@ +// 存储工具类 - 对应Flutter的storage_util.dart +class StorageUtil { + constructor() { + this.prefix = 'lanmei_'; // 应用前缀,避免与其他应用冲突 + } + + // 初始化 + init() { + console.log('存储工具初始化完成'); + } + + // 获取带前缀的key + getKey(key) { + return this.prefix + key; + } + + // 设置数据 + async set(key, value) { + try { + const storageKey = this.getKey(key); + const data = { + value: value, + timestamp: Date.now() + }; + + wx.setStorageSync(storageKey, data); + console.log(`存储数据: ${key} =`, value); + return true; + } catch (error) { + console.error(`存储数据失败: ${key}`, error); + return false; + } + } + + // 获取数据 + async get(key, defaultValue = null) { + try { + const storageKey = this.getKey(key); + const data = wx.getStorageSync(storageKey); + + if (data && data.value !== undefined) { + console.log(`读取数据: ${key} =`, data.value); + return data.value; + } else { + console.log(`读取数据: ${key} = 默认值`, defaultValue); + return defaultValue; + } + } catch (error) { + console.error(`读取数据失败: ${key}`, error); + return defaultValue; + } + } + + // 删除数据 + async remove(key) { + try { + const storageKey = this.getKey(key); + wx.removeStorageSync(storageKey); + console.log(`删除数据: ${key}`); + return true; + } catch (error) { + console.error(`删除数据失败: ${key}`, error); + return false; + } + } + + // 清除所有数据 + async clear() { + try { + const storageInfo = wx.getStorageInfoSync(); + const keys = storageInfo.keys.filter(key => key.startsWith(this.prefix)); + + keys.forEach(key => { + wx.removeStorageSync(key); + }); + + console.log(`清除所有数据: ${keys.length} 项`); + return true; + } catch (error) { + console.error('清除数据失败:', error); + return false; + } + } + + // 检查数据是否存在 + async exists(key) { + try { + const storageKey = this.getKey(key); + const data = wx.getStorageSync(storageKey); + return data !== '' && data !== null && data !== undefined; + } catch (error) { + console.error(`检查数据存在失败: ${key}`, error); + return false; + } + } + + // 获取存储信息 + async getInfo() { + try { + const storageInfo = wx.getStorageInfoSync(); + const appKeys = storageInfo.keys.filter(key => key.startsWith(this.prefix)); + + return { + totalKeys: storageInfo.keys.length, + appKeys: appKeys.length, + currentSize: storageInfo.currentSize, + limitSize: storageInfo.limitSize, + keys: appKeys.map(key => key.replace(this.prefix, '')) + }; + } catch (error) { + console.error('获取存储信息失败:', error); + return null; + } + } + + // 设置过期数据 + async setWithExpire(key, value, expireTime) { + try { + const storageKey = this.getKey(key); + const data = { + value: value, + timestamp: Date.now(), + expire: Date.now() + expireTime * 1000 // expireTime为秒数 + }; + + wx.setStorageSync(storageKey, data); + console.log(`存储带过期时间的数据: ${key} =`, value, `过期时间: ${expireTime}秒`); + return true; + } catch (error) { + console.error(`存储带过期时间的数据失败: ${key}`, error); + return false; + } + } + + // 获取数据(检查过期时间) + async getWithExpireCheck(key, defaultValue = null) { + try { + const storageKey = this.getKey(key); + const data = wx.getStorageSync(storageKey); + + if (!data || data.value === undefined) { + return defaultValue; + } + + // 检查是否过期 + if (data.expire && Date.now() > data.expire) { + console.log(`数据已过期,删除: ${key}`); + await this.remove(key); + return defaultValue; + } + + console.log(`读取数据: ${key} =`, data.value); + return data.value; + } catch (error) { + console.error(`读取数据失败: ${key}`, error); + return defaultValue; + } + } + + // 清理过期数据 + async clearExpiredData() { + try { + const storageInfo = wx.getStorageInfoSync(); + const appKeys = storageInfo.keys.filter(key => key.startsWith(this.prefix)); + let clearedCount = 0; + + appKeys.forEach(storageKey => { + try { + const data = wx.getStorageSync(storageKey); + if (data && data.expire && Date.now() > data.expire) { + wx.removeStorageSync(storageKey); + clearedCount++; + console.log(`清理过期数据: ${storageKey}`); + } + } catch (error) { + console.error(`清理过期数据失败: ${storageKey}`, error); + } + }); + + console.log(`清理过期数据完成: ${clearedCount} 项`); + return clearedCount; + } catch (error) { + console.error('清理过期数据失败:', error); + return 0; + } + } + + // 批量设置数据 + async setBatch(dataMap) { + try { + const results = []; + for (const [key, value] of Object.entries(dataMap)) { + const result = await this.set(key, value); + results.push({ key, success: result }); + } + + const successCount = results.filter(r => r.success).length; + console.log(`批量设置数据: ${successCount}/${results.length} 成功`); + return results; + } catch (error) { + console.error('批量设置数据失败:', error); + return []; + } + } + + // 批量获取数据 + async getBatch(keys, defaultValue = null) { + try { + const results = {}; + for (const key of keys) { + results[key] = await this.get(key, defaultValue); + } + + console.log(`批量获取数据: ${keys.length} 项`); + return results; + } catch (error) { + console.error('批量获取数据失败:', error); + return {}; + } + } + + // 用户相关数据操作的便捷方法 + async setUserData(userData) { + return await this.set('userInfo', userData); + } + + async getUserData() { + return await this.get('userInfo'); + } + + async setToken(token) { + return await this.set('token', token); + } + + async getToken() { + return await this.get('token'); + } + + async setCustomId(customId) { + return await this.set('customId', customId); + } + + async getCustomId() { + return await this.get('customId'); + } + + // 应用设置相关 + async setAppSettings(settings) { + return await this.set('appSettings', settings); + } + + async getAppSettings() { + return await this.get('appSettings', { + theme: 'auto', + language: 'zh', + notifications: true, + locationPrivacy: 1, + autoLocation: true + }); + } + + // 缓存数据操作 + async setCacheData(key, data, expireTime = 3600) { + return await this.setWithExpire(`cache_${key}`, data, expireTime); + } + + async getCacheData(key) { + return await this.getWithExpireCheck(`cache_${key}`); + } + + async clearCache() { + try { + const storageInfo = wx.getStorageInfoSync(); + const cacheKeys = storageInfo.keys.filter(key => + key.startsWith(this.prefix + 'cache_') + ); + + cacheKeys.forEach(key => { + wx.removeStorageSync(key); + }); + + console.log(`清除缓存数据: ${cacheKeys.length} 项`); + return cacheKeys.length; + } catch (error) { + console.error('清除缓存数据失败:', error); + return 0; + } + } +} + +// 创建单例 +const storageUtil = new StorageUtil(); + +module.exports = storageUtil; \ No newline at end of file diff --git a/utils/subscribe-message-manager.js b/utils/subscribe-message-manager.js new file mode 100644 index 0000000..d206abb --- /dev/null +++ b/utils/subscribe-message-manager.js @@ -0,0 +1,440 @@ +// 订阅消息管理器 - 微信小程序专用 +// 处理订阅消息的申请、管理和发送 + +/** + * 微信小程序订阅消息管理器 + * 功能: + * 1. 管理订阅消息模板 + * 2. 请求用户订阅权限 + * 3. 检查订阅状态 + * 4. 智能订阅策略 + * 5. 订阅数据统计 + */ +class SubscribeMessageManager { + constructor() { + this.isInitialized = false; + + // 订阅消息模板配置 + this.templates = { + // 新消息通知 + newMessage: { + id: 'template_id_for_new_message', // 需要在微信公众平台配置 + name: '新消息通知', + description: '当您收到新消息时通知您', + keywords: ['发送人', '消息内容', '发送时间'], + required: false, + category: 'message' + }, + + // 好友请求通知 + friendRequest: { + id: 'template_id_for_friend_request', + name: '好友请求通知', + description: '当有人申请添加您为好友时通知您', + keywords: ['申请人', '申请时间', '验证消息'], + required: false, + category: 'social' + }, + + // 系统通知 + systemNotice: { + id: 'template_id_for_system_notice', + name: '系统通知', + description: '重要的系统消息和公告', + keywords: ['通知类型', '通知内容', '通知时间'], + required: true, + category: 'system' + }, + + // 群聊消息 + groupMessage: { + id: 'template_id_for_group_message', + name: '群聊消息通知', + description: '当群聊中有新消息时通知您', + keywords: ['群名称', '发送人', '消息内容'], + required: false, + category: 'message' + }, + + // 活动提醒 + activityReminder: { + id: 'template_id_for_activity_reminder', + name: '活动提醒', + description: '重要活动和事件提醒', + keywords: ['活动名称', '活动时间', '活动地点'], + required: false, + category: 'reminder' + } + }; + + // 订阅状态缓存 + this.subscriptionStatus = new Map(); + + // 订阅策略配置 + this.subscriptionStrategy = { + // 自动请求订阅的场景 + autoRequestScenes: [ + 'first_message_received', + 'first_friend_request', + 'important_system_notice' + ], + + // 批量请求的最大数量 + maxBatchRequest: 3, + + // 请求间隔(毫秒) + requestInterval: 24 * 60 * 60 * 1000, // 24小时 + + // 最大请求次数 + maxRequestTimes: 3 + }; + + this.init(); + } + + // 初始化订阅消息管理器 + async init() { + if (this.isInitialized) return; + + console.log('📝 初始化订阅消息管理器...'); + + try { + // 加载订阅状态 + await this.loadSubscriptionStatus(); + + // 检查模板配置 + this.validateTemplateConfig(); + + this.isInitialized = true; + console.log('✅ 订阅消息管理器初始化完成'); + + } catch (error) { + console.error('❌ 订阅消息管理器初始化失败:', error); + } + } + + // 请求订阅消息权限 + async requestSubscription(templateKeys, options = {}) { + try { + console.log('📝 请求订阅消息权限:', templateKeys); + + // 验证模板键 + const validTemplateKeys = this.validateTemplateKeys(templateKeys); + if (validTemplateKeys.length === 0) { + throw new Error('没有有效的模板键'); + } + + // 获取模板ID + const templateIds = validTemplateKeys.map(key => this.templates[key].id); + + // 检查请求频率限制 + if (!this.canRequestSubscription(templateKeys)) { + console.log('⏰ 请求频率受限,跳过订阅请求'); + return { success: false, reason: 'rate_limited' }; + } + + // 发起订阅请求 + const result = await this.makeSubscriptionRequest(templateIds, options); + + // 更新订阅状态 + await this.updateSubscriptionStatus(validTemplateKeys, result); + + // 记录请求历史 + this.recordSubscriptionRequest(validTemplateKeys); + + return result; + + } catch (error) { + console.error('❌ 请求订阅消息权限失败:', error); + return { success: false, error: error.message }; + } + } + + // 发起订阅请求 + async makeSubscriptionRequest(templateIds, options = {}) { + return new Promise((resolve) => { + wx.requestSubscribeMessage({ + tmplIds: templateIds, + success: (res) => { + console.log('📝 订阅请求成功:', res); + resolve({ + success: true, + result: res, + acceptedCount: this.countAcceptedSubscriptions(res) + }); + }, + fail: (error) => { + console.error('❌ 订阅请求失败:', error); + resolve({ + success: false, + error: error.errMsg || '订阅请求失败' + }); + } + }); + }); + } + + // 统计接受的订阅数量 + countAcceptedSubscriptions(result) { + let count = 0; + for (const templateId in result) { + if (result[templateId] === 'accept') { + count++; + } + } + return count; + } + + // 智能订阅策略 + async smartSubscriptionRequest(scene, context = {}) { + try { + console.log('🧠 智能订阅策略:', scene); + + // 检查是否需要自动请求 + if (!this.subscriptionStrategy.autoRequestScenes.includes(scene)) { + console.log('📝 当前场景不需要自动请求订阅'); + return; + } + + // 根据场景选择合适的模板 + const templateKeys = this.getTemplatesForScene(scene); + if (templateKeys.length === 0) { + console.log('📝 当前场景没有对应的模板'); + return; + } + + // 过滤已订阅的模板 + const unsubscribedTemplates = templateKeys.filter(key => + !this.isSubscribed(key) + ); + + if (unsubscribedTemplates.length === 0) { + console.log('📝 所有相关模板都已订阅'); + return; + } + + // 限制批量请求数量 + const templatesToRequest = unsubscribedTemplates.slice( + 0, + this.subscriptionStrategy.maxBatchRequest + ); + + // 显示友好的订阅引导 + const shouldRequest = await this.showSubscriptionGuide(scene, templatesToRequest); + if (!shouldRequest) { + console.log('📝 用户取消订阅请求'); + return; + } + + // 发起订阅请求 + return await this.requestSubscription(templatesToRequest, { scene }); + + } catch (error) { + console.error('❌ 智能订阅策略执行失败:', error); + } + } + + // 根据场景获取模板 + getTemplatesForScene(scene) { + switch (scene) { + case 'first_message_received': + return ['newMessage', 'groupMessage']; + case 'first_friend_request': + return ['friendRequest']; + case 'important_system_notice': + return ['systemNotice']; + default: + return []; + } + } + + // 显示订阅引导 + async showSubscriptionGuide(scene, templateKeys) { + const templateNames = templateKeys.map(key => this.templates[key].name); + + return new Promise((resolve) => { + wx.showModal({ + title: '消息通知', + content: `为了及时通知您重要消息,建议开启以下通知:\n${templateNames.join('、')}`, + confirmText: '开启通知', + cancelText: '暂不开启', + success: (res) => { + resolve(res.confirm); + }, + fail: () => { + resolve(false); + } + }); + }); + } + + // 检查是否可以请求订阅 + canRequestSubscription(templateKeys) { + const now = Date.now(); + + for (const key of templateKeys) { + const lastRequest = this.getLastRequestTime(key); + const requestCount = this.getRequestCount(key); + + // 检查请求间隔 + if (lastRequest && (now - lastRequest) < this.subscriptionStrategy.requestInterval) { + return false; + } + + // 检查请求次数 + if (requestCount >= this.subscriptionStrategy.maxRequestTimes) { + return false; + } + } + + return true; + } + + // 验证模板键 + validateTemplateKeys(templateKeys) { + const keys = Array.isArray(templateKeys) ? templateKeys : [templateKeys]; + return keys.filter(key => this.templates[key]); + } + + // 验证模板配置 + validateTemplateConfig() { + for (const [key, template] of Object.entries(this.templates)) { + if (!template.id || template.id.startsWith('template_id_')) { + console.warn(`⚠️ 模板 ${key} 的ID未配置或为占位符`); + } + } + } + + // 检查订阅状态 + isSubscribed(templateKey) { + return this.subscriptionStatus.get(templateKey) === 'accept'; + } + + // 获取所有订阅状态 + getAllSubscriptionStatus() { + const status = {}; + for (const key in this.templates) { + status[key] = { + template: this.templates[key], + subscribed: this.isSubscribed(key), + lastRequest: this.getLastRequestTime(key), + requestCount: this.getRequestCount(key) + }; + } + return status; + } + + // 更新订阅状态 + async updateSubscriptionStatus(templateKeys, result) { + if (!result.success || !result.result) return; + + for (const key of templateKeys) { + const templateId = this.templates[key].id; + const status = result.result[templateId]; + if (status) { + this.subscriptionStatus.set(key, status); + } + } + + await this.saveSubscriptionStatus(); + } + + // 记录请求历史 + recordSubscriptionRequest(templateKeys) { + const now = Date.now(); + const requestHistory = this.getRequestHistory(); + + for (const key of templateKeys) { + if (!requestHistory[key]) { + requestHistory[key] = { times: [], count: 0 }; + } + requestHistory[key].times.push(now); + requestHistory[key].count++; + } + + this.saveRequestHistory(requestHistory); + } + + // 获取请求历史 + getRequestHistory() { + try { + return wx.getStorageSync('subscriptionRequestHistory') || {}; + } catch (error) { + console.error('❌ 获取请求历史失败:', error); + return {}; + } + } + + // 保存请求历史 + saveRequestHistory(history) { + try { + wx.setStorageSync('subscriptionRequestHistory', history); + } catch (error) { + console.error('❌ 保存请求历史失败:', error); + } + } + + // 获取最后请求时间 + getLastRequestTime(templateKey) { + const history = this.getRequestHistory(); + const keyHistory = history[templateKey]; + if (keyHistory && keyHistory.times.length > 0) { + return Math.max(...keyHistory.times); + } + return null; + } + + // 获取请求次数 + getRequestCount(templateKey) { + const history = this.getRequestHistory(); + return history[templateKey]?.count || 0; + } + + // 加载订阅状态 + async loadSubscriptionStatus() { + try { + const status = wx.getStorageSync('subscriptionStatus') || {}; + this.subscriptionStatus = new Map(Object.entries(status)); + } catch (error) { + console.error('❌ 加载订阅状态失败:', error); + } + } + + // 保存订阅状态 + async saveSubscriptionStatus() { + try { + const status = Object.fromEntries(this.subscriptionStatus); + wx.setStorageSync('subscriptionStatus', status); + } catch (error) { + console.error('❌ 保存订阅状态失败:', error); + } + } + + // 获取管理器状态 + getStatus() { + return { + isInitialized: this.isInitialized, + templates: this.templates, + subscriptionStatus: Object.fromEntries(this.subscriptionStatus), + strategy: this.subscriptionStrategy + }; + } + + // 重置订阅状态 + reset() { + this.subscriptionStatus.clear(); + this.saveSubscriptionStatus(); + + // 清除请求历史 + try { + wx.removeStorageSync('subscriptionRequestHistory'); + } catch (error) { + console.error('❌ 清除请求历史失败:', error); + } + } +} + +// 创建全局实例 +const subscribeMessageManager = new SubscribeMessageManager(); + +module.exports = subscribeMessageManager; diff --git a/utils/system-info-helper.js b/utils/system-info-helper.js new file mode 100644 index 0000000..479a338 --- /dev/null +++ b/utils/system-info-helper.js @@ -0,0 +1,247 @@ +/** + * 系统信息工具类 + * 使用新API替换废弃的wx.getSystemInfoSync + */ + +class SystemInfoHelper { + constructor() { + this.cachedInfo = null; + this.cacheTime = 0; + this.cacheExpiry = 5 * 60 * 1000; // 5分钟缓存 + } + + /** + * 获取完整的系统信息(新API版本) + */ + async getSystemInfo() { + // 检查缓存 + if (this.cachedInfo && (Date.now() - this.cacheTime) < this.cacheExpiry) { + return this.cachedInfo; + } + + try { + // 并行获取各种系统信息 + const [windowInfo, deviceInfo, appBaseInfo] = await Promise.all([ + this.getWindowInfo(), + this.getDeviceInfo(), + this.getAppBaseInfo() + ]); + + // 🔥 合并所有信息 - 使用Object.assign替代扩展运算符,避免Babel依赖问题 + const systemInfo = Object.assign({}, windowInfo, deviceInfo, appBaseInfo, { + // 保持向后兼容的字段名 + windowHeight: windowInfo.windowHeight, + windowWidth: windowInfo.windowWidth, + statusBarHeight: windowInfo.statusBarHeight, + safeArea: windowInfo.safeArea, + platform: deviceInfo.platform, + system: deviceInfo.system, + model: deviceInfo.model, + brand: deviceInfo.brand, + version: appBaseInfo.version, + SDKVersion: appBaseInfo.SDKVersion, + language: appBaseInfo.language, + theme: appBaseInfo.theme + }); + + // 缓存结果 + this.cachedInfo = systemInfo; + this.cacheTime = Date.now(); + + return systemInfo; + } catch (error) { + console.warn('获取系统信息失败,使用兜底方案:', error); + return this.getFallbackSystemInfo(); + } + } + + /** + * 同步获取系统信息(用于替换wx.getSystemInfoSync) + */ + getSystemInfoSync() { + // 如果有缓存,直接返回 + if (this.cachedInfo && (Date.now() - this.cacheTime) < this.cacheExpiry) { + return this.cachedInfo; + } + + try { + // 尝试使用新的同步API + const windowInfo = wx.getWindowInfo(); + const deviceInfo = wx.getDeviceInfo(); + const appBaseInfo = wx.getAppBaseInfo(); + + const systemInfo = { + ...windowInfo, + ...deviceInfo, + ...appBaseInfo, + // 保持向后兼容 + windowHeight: windowInfo.windowHeight, + windowWidth: windowInfo.windowWidth, + statusBarHeight: windowInfo.statusBarHeight, + safeArea: windowInfo.safeArea, + platform: deviceInfo.platform, + system: deviceInfo.system, + model: deviceInfo.model, + brand: deviceInfo.brand, + version: appBaseInfo.version, + SDKVersion: appBaseInfo.SDKVersion, + language: appBaseInfo.language, + theme: appBaseInfo.theme + }; + + // 缓存结果 + this.cachedInfo = systemInfo; + this.cacheTime = Date.now(); + + return systemInfo; + } catch (error) { + console.warn('新API获取系统信息失败,使用兜底方案:', error); + return this.getFallbackSystemInfo(); + } + } + + /** + * 获取窗口信息 + */ + getWindowInfo() { + return new Promise((resolve) => { + try { + const windowInfo = wx.getWindowInfo(); + resolve(windowInfo); + } catch (error) { + console.warn('获取窗口信息失败:', error); + resolve({ + windowHeight: 667, + windowWidth: 375, + statusBarHeight: 44, + safeArea: { top: 44, bottom: 667 } + }); + } + }); + } + + /** + * 获取设备信息 + */ + getDeviceInfo() { + return new Promise((resolve) => { + try { + const deviceInfo = wx.getDeviceInfo(); + resolve(deviceInfo); + } catch (error) { + console.warn('获取设备信息失败:', error); + resolve({ + platform: 'unknown', + system: 'unknown', + model: 'unknown', + brand: 'unknown', + benchmarkLevel: 1 + }); + } + }); + } + + /** + * 获取应用基础信息 + */ + getAppBaseInfo() { + return new Promise((resolve) => { + try { + const appBaseInfo = wx.getAppBaseInfo(); + resolve(appBaseInfo); + } catch (error) { + console.warn('获取应用信息失败:', error); + resolve({ + version: '1.0.0', + SDKVersion: '2.0.0', + language: 'zh_CN', + theme: 'light' + }); + } + }); + } + + /** + * 兜底系统信息 + */ + getFallbackSystemInfo() { + try { + // 最后的兜底方案:使用旧API + return wx.getSystemInfoSync(); + } catch (error) { + console.error('所有获取系统信息的方法都失败了:', error); + // 返回默认值 + return { + windowHeight: 667, + windowWidth: 375, + statusBarHeight: 44, + safeArea: { top: 44, bottom: 667 }, + platform: 'unknown', + system: 'unknown', + model: 'unknown', + brand: 'unknown', + version: '1.0.0', + SDKVersion: '2.0.0', + language: 'zh_CN', + theme: 'light' + }; + } + } + + /** + * 清除缓存 + */ + clearCache() { + this.cachedInfo = null; + this.cacheTime = 0; + } + + /** + * 获取菜单按钮信息 + */ + getMenuButtonBoundingClientRect() { + try { + return wx.getMenuButtonBoundingClientRect(); + } catch (error) { + console.warn('获取菜单按钮信息失败:', error); + return { + width: 87, + height: 32, + top: 48, + right: 365, + bottom: 80, + left: 278 + }; + } + } + + /** + * 计算导航栏高度 + */ + getNavBarHeight(systemInfo) { + try { + const menuButton = this.getMenuButtonBoundingClientRect(); + const statusBarHeight = systemInfo.statusBarHeight || 44; + + // 导航栏高度 = 状态栏高度 + 胶囊按钮高度 + 额外间距 + const navBarHeight = statusBarHeight + menuButton.height + + (menuButton.top - statusBarHeight) * 2; + + return navBarHeight; + } catch (error) { + console.warn('计算导航栏高度失败:', error); + return 88; // 默认导航栏高度 + } + } +} + +// 创建全局实例 +const systemInfoHelper = new SystemInfoHelper(); + +module.exports = { + SystemInfoHelper, + systemInfoHelper, + // 向后兼容的函数 + getSystemInfo: () => systemInfoHelper.getSystemInfo(), + getSystemInfoSync: () => systemInfoHelper.getSystemInfoSync() +}; \ No newline at end of file diff --git a/utils/system-info-modern.js b/utils/system-info-modern.js new file mode 100644 index 0000000..538ee88 --- /dev/null +++ b/utils/system-info-modern.js @@ -0,0 +1,241 @@ +/** + * 现代化系统信息工具类 + * 使用新的API替代已弃用的wx.getSystemInfoSync + */ + +class ModernSystemInfo { + constructor() { + this.systemInfo = null; + this.isInitialized = false; + } + + /** + * 获取完整的系统信息(推荐使用) + */ + getSystemInfo() { + try { + // 使用新的API获取系统信息 + const windowInfo = wx.getWindowInfo(); + const deviceInfo = wx.getDeviceInfo(); + const appBaseInfo = wx.getAppBaseInfo(); + + // 合并所有信息 + const systemInfo = { + ...windowInfo, + ...deviceInfo, + ...appBaseInfo, + // 添加一些常用的计算属性 + isIOS: deviceInfo.platform === 'ios', + isAndroid: deviceInfo.platform === 'android', + isDevtools: deviceInfo.platform === 'devtools' + }; + + this.systemInfo = systemInfo; + this.isInitialized = true; + + return systemInfo; + + } catch (error) { + console.error('获取系统信息失败,使用兜底方案:', error); + return this.getFallbackSystemInfo(); + } + } + + /** + * 兜底方案:使用旧API + */ + getFallbackSystemInfo() { + try { + const systemInfo = wx.getSystemInfoSync(); + this.systemInfo = { + ...systemInfo, + isIOS: systemInfo.platform === 'ios', + isAndroid: systemInfo.platform === 'android', + isDevtools: systemInfo.platform === 'devtools' + }; + this.isInitialized = true; + return this.systemInfo; + } catch (error) { + console.error('兜底方案也失败了:', error); + // 返回默认值 + return { + statusBarHeight: 44, + windowHeight: 667, + windowWidth: 375, + screenHeight: 667, + screenWidth: 375, + platform: 'unknown', + system: 'unknown', + version: 'unknown', + isIOS: false, + isAndroid: false, + isDevtools: false + }; + } + } + + /** + * 获取导航栏相关信息 + */ + getNavigationInfo() { + try { + const systemInfo = this.systemInfo || this.getSystemInfo(); + const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); + + const statusBarHeight = systemInfo.statusBarHeight; + const menuButtonHeight = menuButtonInfo.height; + const menuButtonTop = menuButtonInfo.top; + const menuButtonBottom = menuButtonInfo.bottom; + + // 导航栏高度计算 + const navBarHeight = menuButtonBottom + menuButtonTop - statusBarHeight; + + return { + statusBarHeight, + menuButtonHeight, + menuButtonTop, + menuButtonBottom, + navBarHeight, + menuButtonInfo + }; + } catch (error) { + console.error('获取导航栏信息失败:', error); + return { + statusBarHeight: 44, + menuButtonHeight: 32, + menuButtonTop: 6, + menuButtonBottom: 38, + navBarHeight: 88, + menuButtonInfo: {} + }; + } + } + + /** + * 获取安全区域信息 + */ + getSafeAreaInfo() { + try { + const systemInfo = this.systemInfo || this.getSystemInfo(); + const safeArea = systemInfo.safeArea || {}; + + return { + safeAreaTop: safeArea.top || 0, + safeAreaBottom: systemInfo.screenHeight ? systemInfo.screenHeight - safeArea.bottom : 0, + safeAreaLeft: safeArea.left || 0, + safeAreaRight: systemInfo.screenWidth ? systemInfo.screenWidth - safeArea.right : 0, + safeArea: safeArea + }; + } catch (error) { + console.error('获取安全区域信息失败:', error); + return { + safeAreaTop: 0, + safeAreaBottom: 0, + safeAreaLeft: 0, + safeAreaRight: 0, + safeArea: {} + }; + } + } + + /** + * 一次性获取页面所需的所有系统信息 + */ + getPageSystemInfo() { + const systemInfo = this.getSystemInfo(); + const navigationInfo = this.getNavigationInfo(); + const safeAreaInfo = this.getSafeAreaInfo(); + + return { + ...systemInfo, + ...navigationInfo, + ...safeAreaInfo + }; + } +} + +// 创建单例实例 +const modernSystemInfo = new ModernSystemInfo(); + +/** + * 简化的页面系统信息初始化函数 + * 用于快速替换页面中的 wx.getSystemInfoSync() 调用 + */ +function initPageSystemInfo() { + try { + const windowInfo = wx.getWindowInfo(); + const deviceInfo = wx.getDeviceInfo(); + const appBaseInfo = wx.getAppBaseInfo(); + const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); + + // 合并系统信息 + const systemInfo = { + ...windowInfo, + ...deviceInfo, + ...appBaseInfo + }; + + // 计算导航栏相关信息 + const statusBarHeight = windowInfo.statusBarHeight; + const menuButtonHeight = menuButtonInfo.height; + const menuButtonTop = menuButtonInfo.top; + const menuButtonBottom = menuButtonInfo.bottom; + const navBarHeight = menuButtonBottom + menuButtonTop - statusBarHeight; + const windowHeight = windowInfo.windowHeight; + const safeAreaBottom = windowInfo.safeArea ? windowInfo.screenHeight - windowInfo.safeArea.bottom : 0; + + return { + systemInfo, + statusBarHeight, + menuButtonHeight, + menuButtonTop, + navBarHeight, + windowHeight, + safeAreaBottom, + menuButtonInfo + }; + } catch (error) { + console.error('现代API获取系统信息失败,使用兜底方案:', error); + // 兜底方案 + try { + const systemInfo = wx.getSystemInfoSync(); + const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); + + const statusBarHeight = systemInfo.statusBarHeight; + const menuButtonHeight = menuButtonInfo.height; + const menuButtonTop = menuButtonInfo.top; + const menuButtonBottom = menuButtonInfo.bottom; + const navBarHeight = menuButtonBottom + menuButtonTop - statusBarHeight; + const windowHeight = systemInfo.windowHeight; + const safeAreaBottom = systemInfo.safeArea ? systemInfo.screenHeight - systemInfo.safeArea.bottom : 0; + + return { + systemInfo, + statusBarHeight, + menuButtonHeight, + menuButtonTop, + navBarHeight, + windowHeight, + safeAreaBottom, + menuButtonInfo + }; + } catch (fallbackError) { + console.error('兜底方案也失败了:', fallbackError); + return { + systemInfo: {}, + statusBarHeight: 44, + menuButtonHeight: 32, + menuButtonTop: 6, + navBarHeight: 88, + windowHeight: 667, + safeAreaBottom: 0, + menuButtonInfo: {} + }; + } + } +} + +module.exports = { + modernSystemInfo, + initPageSystemInfo +}; \ No newline at end of file diff --git a/utils/system-info.js b/utils/system-info.js new file mode 100644 index 0000000..afdadf0 --- /dev/null +++ b/utils/system-info.js @@ -0,0 +1,129 @@ +// 系统信息工具类 - 使用新API替换废弃的wx.getSystemInfoSync +class SystemInfoUtil { + constructor() { + this.systemInfo = null; + this.windowInfo = null; + this.deviceInfo = null; + this.appBaseInfo = null; + } + + // 初始化系统信息 + async init() { + try { + // 使用新的API获取系统信息 + const [windowInfo, deviceInfo, appBaseInfo] = await Promise.all([ + this.getWindowInfo(), + this.getDeviceInfo(), + this.getAppBaseInfo() + ]); + + this.windowInfo = windowInfo; + this.deviceInfo = deviceInfo; + this.appBaseInfo = appBaseInfo; + + // 合并为兼容的systemInfo格式 + this.systemInfo = { + ...windowInfo, + ...deviceInfo, + ...appBaseInfo + }; + + console.log('系统信息初始化完成:', this.systemInfo); + return this.systemInfo; + } catch (error) { + console.error('系统信息初始化失败:', error); + // 兜底使用旧API + this.systemInfo = wx.getSystemInfoSync(); + return this.systemInfo; + } + } + + // 获取窗口信息 + getWindowInfo() { + return new Promise((resolve, reject) => { + try { + const windowInfo = wx.getWindowInfo(); + resolve(windowInfo); + } catch (error) { + reject(error); + } + }); + } + + // 获取设备信息 + getDeviceInfo() { + return new Promise((resolve, reject) => { + try { + const deviceInfo = wx.getDeviceInfo(); + resolve(deviceInfo); + } catch (error) { + reject(error); + } + }); + } + + // 获取应用基础信息 + getAppBaseInfo() { + return new Promise((resolve, reject) => { + try { + const appBaseInfo = wx.getAppBaseInfo(); + resolve(appBaseInfo); + } catch (error) { + reject(error); + } + }); + } + + // 获取系统适配信息 + getSystemAdaptInfo() { + if (!this.systemInfo) { + console.warn('系统信息未初始化,使用同步API'); + this.systemInfo = wx.getSystemInfoSync(); + } + + const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); + + // 状态栏高度 + const statusBarHeight = this.systemInfo.statusBarHeight || 0; + + // 胶囊按钮信息 + const menuButtonHeight = menuButtonInfo.height; + const menuButtonTop = menuButtonInfo.top; + const menuButtonBottom = menuButtonInfo.bottom; + + // 导航栏高度 = 胶囊按钮底部 + 胶囊按钮顶部到状态栏的距离 + const navBarHeight = menuButtonBottom + menuButtonTop - statusBarHeight; + + // 窗口高度 + const windowHeight = this.systemInfo.windowHeight || this.systemInfo.screenHeight; + + // 安全区域 + const safeAreaBottom = this.systemInfo.safeArea ? + this.systemInfo.screenHeight - this.systemInfo.safeArea.bottom : 0; + + return { + systemInfo: this.systemInfo, + statusBarHeight, + menuButtonHeight, + menuButtonTop, + navBarHeight, + windowHeight, + safeAreaBottom + }; + } + + // 获取性能信息 + getPerformanceInfo() { + return { + platform: this.systemInfo?.platform || 'unknown', + version: this.systemInfo?.version || 'unknown', + SDKVersion: this.systemInfo?.SDKVersion || 'unknown', + benchmarkLevel: this.systemInfo?.benchmarkLevel || 0 + }; + } +} + +// 创建单例 +const systemInfoUtil = new SystemInfoUtil(); + +module.exports = systemInfoUtil; \ No newline at end of file diff --git a/utils/ui-helper.js b/utils/ui-helper.js new file mode 100644 index 0000000..0b56119 --- /dev/null +++ b/utils/ui-helper.js @@ -0,0 +1,365 @@ +// UI辅助工具 - 全局用户体验优化 +class UIHelper { + constructor() { + this.loadingCount = 0; + this.toastQueue = []; + this.isShowingToast = false; + } + + // 🔥 ===== 加载状态管理 ===== + + // 显示加载 + showLoading(title = '加载中...', mask = true) { + this.loadingCount++; + + if (this.loadingCount === 1) { + wx.showLoading({ + title: title, + mask: mask + }); + } + } + + // 隐藏加载 + hideLoading() { + this.loadingCount = Math.max(0, this.loadingCount - 1); + + if (this.loadingCount === 0) { + wx.hideLoading(); + } + } + + // 强制隐藏加载 + forceHideLoading() { + this.loadingCount = 0; + wx.hideLoading(); + } + + // 🔥 ===== 消息提示管理 ===== + + // 显示成功消息 + showSuccess(title, duration = 2000) { + this.showToast({ + title: title, + icon: 'success', + duration: duration + }); + } + + // 显示错误消息 + showError(title, duration = 3000) { + this.showToast({ + title: title, + icon: 'error', + duration: duration + }); + } + + // 显示警告消息 + showWarning(title, duration = 2500) { + this.showToast({ + title: title, + icon: 'none', + duration: duration + }); + } + + // 显示信息消息 + showInfo(title, duration = 2000) { + this.showToast({ + title: title, + icon: 'none', + duration: duration + }); + } + + // 队列化Toast显示 + showToast(options) { + this.toastQueue.push(options); + this.processToastQueue(); + } + + // 处理Toast队列 + processToastQueue() { + if (this.isShowingToast || this.toastQueue.length === 0) { + return; + } + + this.isShowingToast = true; + const options = this.toastQueue.shift(); + + wx.showToast({ + ...options, + success: () => { + setTimeout(() => { + this.isShowingToast = false; + this.processToastQueue(); + }, options.duration || 2000); + }, + fail: () => { + this.isShowingToast = false; + this.processToastQueue(); + } + }); + } + + // 🔥 ===== 模态框管理 ===== + + // 显示确认对话框 + showConfirm(options) { + return new Promise((resolve) => { + wx.showModal({ + title: options.title || '提示', + content: options.content || '', + showCancel: options.showCancel !== false, + cancelText: options.cancelText || '取消', + confirmText: options.confirmText || '确定', + cancelColor: options.cancelColor || '#666666', + confirmColor: options.confirmColor || '#4CAF50', + success: (res) => { + resolve(res.confirm); + }, + fail: () => { + resolve(false); + } + }); + }); + } + + // 显示操作菜单 + showActionSheet(options) { + return new Promise((resolve) => { + wx.showActionSheet({ + itemList: options.itemList || [], + itemColor: options.itemColor || '#000000', + success: (res) => { + resolve(res.tapIndex); + }, + fail: () => { + resolve(-1); + } + }); + }); + } + + // 🔥 ===== 网络状态管理 ===== + + // 检查网络状态 + async checkNetworkStatus() { + try { + const networkInfo = await this.getNetworkType(); + + if (networkInfo.networkType === 'none') { + this.showError('网络连接不可用,请检查网络设置'); + return false; + } + + return true; + } catch (error) { + console.error('检查网络状态失败:', error); + return true; // 默认认为网络可用 + } + } + + // 获取网络类型 + getNetworkType() { + return new Promise((resolve, reject) => { + wx.getNetworkType({ + success: resolve, + fail: reject + }); + }); + } + + // 🔥 ===== 页面导航管理 ===== + + // 安全导航到页面 + navigateTo(url, options = {}) { + // 检查URL格式 + if (!url || typeof url !== 'string') { + this.showError('页面地址无效'); + return Promise.reject(new Error('Invalid URL')); + } + + return new Promise((resolve, reject) => { + wx.navigateTo({ + url: url, + success: resolve, + fail: (error) => { + console.error('页面导航失败:', error); + + // 如果是页面栈满了,尝试重定向 + if (error.errMsg && error.errMsg.includes('limit exceed')) { + wx.redirectTo({ + url: url, + success: resolve, + fail: reject + }); + } else { + this.showError('页面跳转失败'); + reject(error); + } + } + }); + }); + } + + // 安全重定向到页面 + redirectTo(url) { + return new Promise((resolve, reject) => { + wx.redirectTo({ + url: url, + success: resolve, + fail: (error) => { + console.error('页面重定向失败:', error); + this.showError('页面跳转失败'); + reject(error); + } + }); + }); + } + + // 安全重启到页面 + reLaunch(url) { + return new Promise((resolve, reject) => { + wx.reLaunch({ + url: url, + success: resolve, + fail: (error) => { + console.error('页面重启失败:', error); + this.showError('页面跳转失败'); + reject(error); + } + }); + }); + } + + // 🔥 ===== 错误处理 ===== + + // 处理API错误 + handleApiError(error, defaultMessage = '操作失败,请重试') { + console.error('API错误:', error); + + let message = defaultMessage; + + if (error && error.message) { + message = error.message; + } else if (typeof error === 'string') { + message = error; + } + + // 特殊错误处理 + if (message.includes('网络')) { + this.showError('网络连接异常,请检查网络设置'); + } else if (message.includes('登录') || message.includes('认证') || message.includes('token')) { + this.showError('登录已过期,请重新登录'); + // 可以在这里触发重新登录逻辑 + } else { + this.showError(message); + } + } + + // 🔥 ===== 工具方法 ===== + + // 防抖函数 + debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } + + // 节流函数 + throttle(func, limit) { + let inThrottle; + return function executedFunction(...args) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; + } + + // 格式化文件大小 + formatFileSize(bytes) { + if (bytes === 0) return '0 B'; + + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + // 格式化时间 + formatTime(timestamp) { + const now = new Date(); + const time = new Date(timestamp); + const diff = now - time; + + // 一分钟内 + if (diff < 60000) { + return '刚刚'; + } + + // 一小时内 + if (diff < 3600000) { + return Math.floor(diff / 60000) + '分钟前'; + } + + // 今天 + if (now.toDateString() === time.toDateString()) { + return time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); + } + + // 昨天 + const yesterday = new Date(now); + yesterday.setDate(yesterday.getDate() - 1); + if (yesterday.toDateString() === time.toDateString()) { + return '昨天 ' + time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); + } + + // 其他 + return time.toLocaleDateString('zh-CN') + ' ' + time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); + } + + // 复制到剪贴板 + copyToClipboard(text) { + return new Promise((resolve, reject) => { + wx.setClipboardData({ + data: text, + success: () => { + this.showSuccess('已复制到剪贴板'); + resolve(); + }, + fail: (error) => { + this.showError('复制失败'); + reject(error); + } + }); + }); + } + + // 震动反馈 + vibrateShort() { + wx.vibrateShort({ + type: 'light' + }); + } + + // 震动反馈(长) + vibrateLong() { + wx.vibrateLong(); + } +} + +// 创建全局单例 +const uiHelper = new UIHelper(); + +module.exports = uiHelper; diff --git a/utils/voice-message-manager.js b/utils/voice-message-manager.js new file mode 100644 index 0000000..55c6746 --- /dev/null +++ b/utils/voice-message-manager.js @@ -0,0 +1,751 @@ +// 语音消息管理器 - 微信小程序专用 +// 处理语音录制、播放、转换、上传等功能 + +const apiClient = require('./api-client.js'); +const performanceMonitor = require('./performance-monitor.js'); + +/** + * 语音消息管理器 + * 功能: + * 1. 语音录制和停止 + * 2. 语音播放和暂停 + * 3. 语音文件管理 + * 4. 语音质量控制 + * 5. 语音时长限制 + * 6. 语音格式转换 + */ +class VoiceMessageManager { + constructor() { + this.isInitialized = false; + + // 录音配置 + this.recordConfig = { + duration: 60000, // 最大录音时长 60秒 + sampleRate: 16000, // 采样率 + numberOfChannels: 1, // 声道数 + encodeBitRate: 48000, // 编码码率 + format: 'mp3', // 录音格式 + frameSize: 50, // 帧大小 + minDuration: 1000, // 最小录音时长 1秒 + maxDuration: 60000 // 最大录音时长 60秒 + }; + + // 播放配置 + this.playConfig = { + autoplay: false, + loop: false, + volume: 1.0, + playbackRate: 1.0 + }; + + // 录音器实例 + this.recorderManager = null; + this.innerAudioContext = null; + + // 录音状态 + this.recordingState = { + isRecording: false, + isPaused: false, + startTime: null, + duration: 0, + tempFilePath: null, + fileSize: 0 + }; + + // 播放状态 + this.playingState = { + isPlaying: false, + isPaused: false, + currentTime: 0, + duration: 0, + currentVoiceId: null, + playingMessageId: null + }; + + // 语音文件缓存 + this.voiceCache = new Map(); + + // 事件监听器 + this.eventListeners = new Map(); + + // 权限状态 + this.permissionGranted = false; + + this.init(); + } + + // 初始化语音消息管理器 + async init() { + if (this.isInitialized) return; + + console.log('🎤 初始化语音消息管理器...'); + + try { + // 初始化录音管理器 + this.initRecorderManager(); + + // 初始化音频播放器 + this.initAudioPlayer(); + + // 检查录音权限 + await this.checkRecordPermission(); + + this.isInitialized = true; + console.log('✅ 语音消息管理器初始化完成'); + + } catch (error) { + console.error('❌ 语音消息管理器初始化失败:', error); + } + } + + // 初始化录音管理器 + initRecorderManager() { + this.recorderManager = wx.getRecorderManager(); + + // 录音开始事件 + this.recorderManager.onStart(() => { + console.log('🎤 录音开始'); + this.recordingState.isRecording = true; + this.recordingState.startTime = Date.now(); + this.triggerEvent('recordStart'); + }); + + // 录音暂停事件 + this.recorderManager.onPause(() => { + console.log('⏸️ 录音暂停'); + this.recordingState.isPaused = true; + this.triggerEvent('recordPause'); + }); + + // 录音恢复事件 + this.recorderManager.onResume(() => { + console.log('▶️ 录音恢复'); + this.recordingState.isPaused = false; + this.triggerEvent('recordResume'); + }); + + // 录音停止事件 + this.recorderManager.onStop((res) => { + console.log('⏹️ 录音停止:', res); + + this.recordingState.isRecording = false; + this.recordingState.isPaused = false; + this.recordingState.duration = res.duration; + this.recordingState.tempFilePath = res.tempFilePath; + this.recordingState.fileSize = res.fileSize; + + this.triggerEvent('recordStop', { + duration: res.duration, + tempFilePath: res.tempFilePath, + fileSize: res.fileSize + }); + }); + + // 录音错误事件 + this.recorderManager.onError((error) => { + console.error('❌ 录音错误:', error); + this.recordingState.isRecording = false; + this.recordingState.isPaused = false; + this.triggerEvent('recordError', error); + }); + + // 录音帧数据事件(用于实时波形显示) + this.recorderManager.onFrameRecorded((res) => { + this.triggerEvent('recordFrame', { + frameBuffer: res.frameBuffer, + isLastFrame: res.isLastFrame + }); + }); + } + + // 初始化音频播放器 + initAudioPlayer() { + this.innerAudioContext = wx.createInnerAudioContext(); + + // 播放开始事件 + this.innerAudioContext.onPlay(() => { + console.log('🔊 语音播放开始'); + this.playingState.isPlaying = true; + this.playingState.isPaused = false; + this.triggerEvent('playStart'); + }); + + // 播放暂停事件 + this.innerAudioContext.onPause(() => { + console.log('⏸️ 语音播放暂停'); + this.playingState.isPaused = true; + this.triggerEvent('playPause'); + }); + + // 播放结束事件 + this.innerAudioContext.onEnded(() => { + console.log('⏹️ 语音播放结束'); + this.playingState.isPlaying = false; + this.playingState.isPaused = false; + this.playingState.currentTime = 0; + this.playingState.currentVoiceId = null; + this.playingState.playingMessageId = null; + this.triggerEvent('playEnd'); + }); + + // 播放错误事件 + this.innerAudioContext.onError((error) => { + console.error('❌ 语音播放错误:', error); + this.playingState.isPlaying = false; + this.playingState.isPaused = false; + this.triggerEvent('playError', error); + }); + + // 播放进度更新事件 + this.innerAudioContext.onTimeUpdate(() => { + this.playingState.currentTime = this.innerAudioContext.currentTime; + this.playingState.duration = this.innerAudioContext.duration; + this.triggerEvent('playTimeUpdate', { + currentTime: this.playingState.currentTime, + duration: this.playingState.duration + }); + }); + + // 音频加载完成事件 + this.innerAudioContext.onCanplay(() => { + console.log('🎵 语音加载完成'); + this.triggerEvent('playCanplay'); + }); + } + + // 🎤 ===== 录音功能 ===== + + // 开始录音 + async startRecording(options = {}) { + if (!this.isInitialized) { + throw new Error('语音消息管理器未初始化'); + } + + if (this.recordingState.isRecording) { + throw new Error('正在录音中'); + } + + // 检查录音权限 + if (!this.permissionGranted) { + const granted = await this.requestRecordPermission(); + if (!granted) { + throw new Error('录音权限被拒绝'); + } + } + + try { + const recordOptions = { + ...this.recordConfig, + ...options + }; + + console.log('🎤 开始录音,配置:', recordOptions); + + // 重置录音状态 + this.recordingState = { + isRecording: false, + isPaused: false, + startTime: null, + duration: 0, + tempFilePath: null, + fileSize: 0 + }; + + this.recorderManager.start(recordOptions); + + } catch (error) { + console.error('❌ 开始录音失败:', error); + throw error; + } + } + + // 停止录音 + stopRecording() { + if (!this.recordingState.isRecording) { + throw new Error('当前没有在录音'); + } + + try { + console.log('⏹️ 停止录音'); + this.recorderManager.stop(); + + } catch (error) { + console.error('❌ 停止录音失败:', error); + throw error; + } + } + + // 暂停录音 + pauseRecording() { + if (!this.recordingState.isRecording || this.recordingState.isPaused) { + throw new Error('当前状态无法暂停录音'); + } + + try { + console.log('⏸️ 暂停录音'); + this.recorderManager.pause(); + + } catch (error) { + console.error('❌ 暂停录音失败:', error); + throw error; + } + } + + // 恢复录音 + resumeRecording() { + if (!this.recordingState.isRecording || !this.recordingState.isPaused) { + throw new Error('当前状态无法恢复录音'); + } + + try { + console.log('▶️ 恢复录音'); + this.recorderManager.resume(); + + } catch (error) { + console.error('❌ 恢复录音失败:', error); + throw error; + } + } + + // 取消录音 + cancelRecording() { + if (!this.recordingState.isRecording) { + return; + } + + try { + console.log('❌ 取消录音'); + this.recorderManager.stop(); + + // 重置录音状态 + this.recordingState = { + isRecording: false, + isPaused: false, + startTime: null, + duration: 0, + tempFilePath: null, + fileSize: 0 + }; + + this.triggerEvent('recordCancel'); + + } catch (error) { + console.error('❌ 取消录音失败:', error); + } + } + + // 🔊 ===== 播放功能 ===== + + // 播放语音消息 + async playVoiceMessage(voiceUrl, messageId = null, options = {}) { + if (!this.isInitialized) { + throw new Error('语音消息管理器未初始化'); + } + + try { + // 如果正在播放其他语音,先停止 + if (this.playingState.isPlaying) { + this.stopPlaying(); + } + + console.log('🔊 播放语音消息:', voiceUrl); + + // 设置播放配置 + const playOptions = { + ...this.playConfig, + ...options + }; + + this.innerAudioContext.src = voiceUrl; + this.innerAudioContext.autoplay = playOptions.autoplay; + this.innerAudioContext.loop = playOptions.loop; + this.innerAudioContext.volume = playOptions.volume; + this.innerAudioContext.playbackRate = playOptions.playbackRate; + + // 更新播放状态 + this.playingState.currentVoiceId = voiceUrl; + this.playingState.playingMessageId = messageId; + + // 开始播放 + this.innerAudioContext.play(); + + } catch (error) { + console.error('❌ 播放语音消息失败:', error); + throw error; + } + } + + // 暂停播放 + pausePlaying() { + if (!this.playingState.isPlaying || this.playingState.isPaused) { + return; + } + + try { + console.log('⏸️ 暂停播放'); + this.innerAudioContext.pause(); + + } catch (error) { + console.error('❌ 暂停播放失败:', error); + } + } + + // 恢复播放 + resumePlaying() { + if (!this.playingState.isPlaying || !this.playingState.isPaused) { + return; + } + + try { + console.log('▶️ 恢复播放'); + this.innerAudioContext.play(); + + } catch (error) { + console.error('❌ 恢复播放失败:', error); + } + } + + // 停止播放 + stopPlaying() { + if (!this.playingState.isPlaying) { + return; + } + + try { + console.log('⏹️ 停止播放'); + this.innerAudioContext.stop(); + + // 重置播放状态 + this.playingState.isPlaying = false; + this.playingState.isPaused = false; + this.playingState.currentTime = 0; + this.playingState.currentVoiceId = null; + this.playingState.playingMessageId = null; + + } catch (error) { + console.error('❌ 停止播放失败:', error); + } + } + + // 设置播放进度 + seekTo(time) { + if (!this.playingState.isPlaying) { + return; + } + + try { + this.innerAudioContext.seek(time); + this.playingState.currentTime = time; + + } catch (error) { + console.error('❌ 设置播放进度失败:', error); + } + } + + // 📁 ===== 文件管理 ===== + + // 上传语音文件 + async uploadVoiceFile(tempFilePath, duration) { + try { + console.log('📤 上传语音文件:', tempFilePath); + + // 使用微信小程序的上传文件API + const uploadResult = await new Promise((resolve, reject) => { + wx.uploadFile({ + url: `${apiClient.baseUrl}/api/v1/file/upload`, + filePath: tempFilePath, + name: 'file', + formData: { + file_type: 'audio', + usage_type: 'message', + duration: duration.toString() + }, + header: { + 'Authorization': `Bearer ${apiClient.getToken()}` + }, + success: (res) => { + try { + const data = JSON.parse(res.data); + resolve({ + success: data.success || res.statusCode === 200, + data: data.data || data, + message: data.message + }); + } catch (error) { + resolve({ + success: res.statusCode === 200, + data: { url: res.data }, + message: '上传成功' + }); + } + }, + fail: reject + }); + }); + + if (uploadResult.success) { + const fileUrl = uploadResult.data.url || uploadResult.data.file_url || uploadResult.data.fileUrl; + console.log('✅ 语音文件上传成功:', fileUrl); + return { + success: true, + url: fileUrl, + duration: duration, + size: this.recordingState.fileSize + }; + } else { + throw new Error(uploadResult.message || '上传失败'); + } + + } catch (error) { + console.error('❌ 上传语音文件失败:', error); + throw error; + } + } + + // 下载语音文件到本地 + async downloadVoiceFile(voiceUrl) { + try { + // 检查缓存 + if (this.voiceCache.has(voiceUrl)) { + const cached = this.voiceCache.get(voiceUrl); + if (this.isFileExists(cached.localPath)) { + return cached.localPath; + } else { + this.voiceCache.delete(voiceUrl); + } + } + + console.log('📥 下载语音文件:', voiceUrl); + + const downloadResult = await new Promise((resolve, reject) => { + wx.downloadFile({ + url: voiceUrl, + success: resolve, + fail: reject + }); + }); + + if (downloadResult.statusCode === 200) { + // 缓存文件路径 + this.voiceCache.set(voiceUrl, { + localPath: downloadResult.tempFilePath, + downloadTime: Date.now() + }); + + console.log('✅ 语音文件下载成功:', downloadResult.tempFilePath); + return downloadResult.tempFilePath; + } else { + throw new Error(`下载失败,状态码: ${downloadResult.statusCode}`); + } + + } catch (error) { + console.error('❌ 下载语音文件失败:', error); + throw error; + } + } + + // 检查文件是否存在 + isFileExists(filePath) { + try { + const fileManager = wx.getFileSystemManager(); + const stats = fileManager.statSync(filePath); + return stats.isFile(); + } catch (error) { + return false; + } + } + + // 🔐 ===== 权限管理 ===== + + // 检查录音权限 + async checkRecordPermission() { + try { + const setting = await new Promise((resolve, reject) => { + wx.getSetting({ + success: resolve, + fail: reject + }); + }); + + this.permissionGranted = setting.authSetting['scope.record'] === true; + console.log('🔐 录音权限状态:', this.permissionGranted); + + return this.permissionGranted; + + } catch (error) { + console.error('❌ 检查录音权限失败:', error); + return false; + } + } + + // 请求录音权限 + async requestRecordPermission() { + try { + await new Promise((resolve, reject) => { + wx.authorize({ + scope: 'scope.record', + success: resolve, + fail: reject + }); + }); + + this.permissionGranted = true; + console.log('✅ 录音权限获取成功'); + return true; + + } catch (error) { + console.error('❌ 录音权限获取失败:', error); + this.permissionGranted = false; + + // 引导用户到设置页面 + this.showPermissionGuide(); + return false; + } + } + + // 显示权限引导 + showPermissionGuide() { + wx.showModal({ + title: '需要录音权限', + content: '使用语音消息功能需要录音权限,请在设置中开启', + confirmText: '去设置', + cancelText: '取消', + success: (res) => { + if (res.confirm) { + wx.openSetting({ + success: (settingRes) => { + if (settingRes.authSetting['scope.record']) { + this.permissionGranted = true; + console.log('✅ 用户已开启录音权限'); + } + } + }); + } + } + }); + } + + // 📊 ===== 状态管理 ===== + + // 获取录音状态 + getRecordingState() { + return { ...this.recordingState }; + } + + // 获取播放状态 + getPlayingState() { + return { ...this.playingState }; + } + + // 是否正在录音 + isRecording() { + return this.recordingState.isRecording; + } + + // 是否正在播放 + isPlaying() { + return this.playingState.isPlaying; + } + + // 获取当前播放的消息ID + getCurrentPlayingMessageId() { + return this.playingState.playingMessageId; + } + + // 🎧 ===== 事件管理 ===== + + // 注册事件监听器 + on(event, callback) { + if (!this.eventListeners.has(event)) { + this.eventListeners.set(event, []); + } + this.eventListeners.get(event).push(callback); + } + + // 移除事件监听器 + off(event, callback) { + if (this.eventListeners.has(event)) { + const listeners = this.eventListeners.get(event); + const index = listeners.indexOf(callback); + if (index > -1) { + listeners.splice(index, 1); + } + } + } + + // 触发事件 + triggerEvent(event, data = null) { + if (this.eventListeners.has(event)) { + const listeners = this.eventListeners.get(event); + listeners.forEach(callback => { + try { + callback(data); + } catch (error) { + console.error(`❌ 事件处理器错误 [${event}]:`, error); + } + }); + } + } + + // 🔧 ===== 工具方法 ===== + + // 格式化时长 + formatDuration(duration) { + const seconds = Math.floor(duration / 1000); + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + + if (minutes > 0) { + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; + } else { + return `${remainingSeconds}"`; + } + } + + // 获取语音文件大小描述 + getFileSizeDescription(fileSize) { + if (fileSize < 1024) { + return `${fileSize}B`; + } else if (fileSize < 1024 * 1024) { + return `${(fileSize / 1024).toFixed(1)}KB`; + } else { + return `${(fileSize / (1024 * 1024)).toFixed(1)}MB`; + } + } + + // 清理缓存 + clearCache() { + this.voiceCache.clear(); + console.log('🧹 语音文件缓存已清理'); + } + + // 销毁管理器 + destroy() { + // 停止录音和播放 + if (this.recordingState.isRecording) { + this.cancelRecording(); + } + + if (this.playingState.isPlaying) { + this.stopPlaying(); + } + + // 销毁音频上下文 + if (this.innerAudioContext) { + this.innerAudioContext.destroy(); + this.innerAudioContext = null; + } + + // 清理缓存和事件监听器 + this.clearCache(); + this.eventListeners.clear(); + + this.isInitialized = false; + console.log('🎤 语音消息管理器已销毁'); + } +} + +// 创建全局实例 +const voiceMessageManager = new VoiceMessageManager(); + +module.exports = voiceMessageManager; diff --git a/utils/websocket-diagnostic.js b/utils/websocket-diagnostic.js new file mode 100644 index 0000000..35a5620 --- /dev/null +++ b/utils/websocket-diagnostic.js @@ -0,0 +1,375 @@ +// WebSocket连接诊断工具 +class WebSocketDiagnostic { + constructor() { + this.testResults = []; + } + + // 🔥 ===== 全面诊断WebSocket连接问题 ===== + + async runFullDiagnostic() { + console.log('🔍 开始WebSocket连接全面诊断...'); + this.testResults = []; + + // 1. 检查基础环境 + await this.checkEnvironment(); + + // 2. 检查认证信息 + await this.checkAuthentication(); + + // 3. 测试网络连接 + await this.testNetworkConnectivity(); + + // 4. 测试WebSocket连接 + await this.testWebSocketConnection(); + + // 5. 生成诊断报告 + this.generateReport(); + + return this.testResults; + } + + // 检查基础环境 + async checkEnvironment() { + console.log('📱 检查小程序环境...'); + + try { + // 使用新的API替代已弃用的wx.getSystemInfoSync + const deviceInfo = wx.getDeviceInfo(); + const appBaseInfo = wx.getAppBaseInfo(); + const accountInfo = wx.getAccountInfoSync(); + + const envInfo = { + platform: deviceInfo.platform, + version: appBaseInfo.version, + SDKVersion: appBaseInfo.SDKVersion, + appId: accountInfo.miniProgram.appId, + envVersion: accountInfo.miniProgram.envVersion + }; + + console.log('📱 环境信息:', envInfo); + + this.testResults.push({ + test: '环境检查', + status: 'success', + data: envInfo + }); + + } catch (error) { + console.error('❌ 环境检查失败:', error); + this.testResults.push({ + test: '环境检查', + status: 'error', + error: error.message + }); + } + } + + // 检查认证信息 + async checkAuthentication() { + console.log('🔑 检查认证信息...'); + + try { + // 获取token + const userInfo = wx.getStorageSync('userInfo'); + const directToken = wx.getStorageSync('token'); + const app = getApp(); + const appToken = app?.globalData?.userInfo?.token; + + const authInfo = { + hasUserInfo: !!userInfo, + hasUserInfoToken: !!(userInfo?.token), + hasDirectToken: !!directToken, + hasAppToken: !!appToken, + userInfoTokenLength: userInfo?.token?.length || 0, + directTokenLength: directToken?.length || 0, + appTokenLength: appToken?.length || 0 + }; + + console.log('🔑 认证信息:', authInfo); + + // 检查token格式 + const token = userInfo?.token || directToken || appToken; + if (token) { + authInfo.tokenPrefix = token.substring(0, 20) + '...'; + authInfo.isJWT = token.startsWith('eyJ'); + authInfo.tokenParts = token.split('.').length; + } + + this.testResults.push({ + test: '认证检查', + status: token ? 'success' : 'error', + data: authInfo, + error: token ? null : '未找到有效的认证token' + }); + + } catch (error) { + console.error('❌ 认证检查失败:', error); + this.testResults.push({ + test: '认证检查', + status: 'error', + error: error.message + }); + } + } + + // 测试网络连接 + async testNetworkConnectivity() { + console.log('🌐 测试网络连接...'); + + try { + // 检查网络状态 + const networkInfo = await this.getNetworkType(); + console.log('🌐 网络状态:', networkInfo); + + // 测试HTTP连接 + const httpTest = await this.testHttpConnection(); + + this.testResults.push({ + test: '网络连接', + status: 'success', + data: { + network: networkInfo, + httpTest: httpTest + } + }); + + } catch (error) { + console.error('❌ 网络连接测试失败:', error); + this.testResults.push({ + test: '网络连接', + status: 'error', + error: error.message + }); + } + } + + // 测试WebSocket连接 + async testWebSocketConnection() { + console.log('🔌 测试WebSocket连接...'); + + const testUrls = [ + 'wss://api.faxianwo.me/api/v1/ws', + 'wss://api.faxianwo.me', + 'wss://api.faxianwo.me/ws' + ]; + + const results = []; + + for (const url of testUrls) { + console.log(`🔗 测试URL: ${url}`); + + try { + const result = await this.testSingleWebSocketUrl(url); + results.push({ + url: url, + status: result.success ? 'success' : 'error', + ...result + }); + } catch (error) { + results.push({ + url: url, + status: 'error', + error: error.message + }); + } + } + + this.testResults.push({ + test: 'WebSocket连接', + status: results.some(r => r.status === 'success') ? 'success' : 'error', + data: results + }); + } + + // 测试单个WebSocket URL + testSingleWebSocketUrl(url) { + return new Promise((resolve) => { + const startTime = Date.now(); + let resolved = false; + + const timeout = setTimeout(() => { + if (!resolved) { + resolved = true; + resolve({ + success: false, + error: '连接超时', + duration: Date.now() - startTime + }); + } + }, 10000); + + try { + // 获取token + const userInfo = wx.getStorageSync('userInfo'); + const token = userInfo?.token; + + const testWs = wx.connectSocket({ + url: `${url}?device_id=diagnostic_${Date.now()}`, + header: token ? { + 'Authorization': `Bearer ${token}` + } : {}, + timeout: 8000 + }); + + testWs.onOpen((res) => { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + testWs.close(); + resolve({ + success: true, + duration: Date.now() - startTime, + response: res + }); + } + }); + + testWs.onError((error) => { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + resolve({ + success: false, + error: error.errMsg || 'WebSocket连接错误', + duration: Date.now() - startTime, + errorDetail: error + }); + } + }); + + testWs.onClose((res) => { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + resolve({ + success: false, + error: '连接被关闭', + duration: Date.now() - startTime, + closeDetail: res + }); + } + }); + + } catch (error) { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + resolve({ + success: false, + error: error.message, + duration: Date.now() - startTime + }); + } + } + }); + } + + // 获取网络类型 + getNetworkType() { + return new Promise((resolve, reject) => { + wx.getNetworkType({ + success: resolve, + fail: reject + }); + }); + } + + // 测试HTTP连接 + testHttpConnection() { + return new Promise((resolve) => { + wx.request({ + url: 'https://api.faxianwo.me/api/v1/health', + method: 'GET', + timeout: 5000, + success: (res) => { + resolve({ + success: true, + statusCode: res.statusCode, + data: res.data + }); + }, + fail: (error) => { + resolve({ + success: false, + error: error.errMsg + }); + } + }); + }); + } + + // 生成诊断报告 + generateReport() { + console.log('📋 生成诊断报告...'); + console.log('='.repeat(50)); + console.log('🔍 WebSocket连接诊断报告'); + console.log('='.repeat(50)); + + this.testResults.forEach((result, index) => { + const status = result.status === 'success' ? '✅' : '❌'; + console.log(`${index + 1}. ${status} ${result.test}`); + + if (result.status === 'error') { + console.log(` 错误: ${result.error}`); + } + + if (result.data) { + console.log(` 数据:`, result.data); + + // 🔥 特别显示WebSocket连接的详细结果 + if (result.test === 'WebSocket连接' && Array.isArray(result.data)) { + result.data.forEach((wsResult, i) => { + const wsStatus = wsResult.status === 'success' ? '✅' : '❌'; + console.log(` ${wsStatus} ${wsResult.url}`); + if (wsResult.status === 'success') { + console.log(` 连接时间: ${wsResult.duration}ms`); + } else { + console.log(` 错误: ${wsResult.error}`); + if (wsResult.errorDetail) { + console.log(` 详情:`, wsResult.errorDetail); + } + } + }); + } + } + + console.log(''); + }); + + // 生成建议 + this.generateSuggestions(); + } + + // 生成修复建议 + generateSuggestions() { + console.log('💡 修复建议:'); + + const authResult = this.testResults.find(r => r.test === '认证检查'); + const wsResult = this.testResults.find(r => r.test === 'WebSocket连接'); + + if (authResult?.status === 'error') { + console.log('1. 🔑 认证问题:请确保用户已正确登录并保存了token'); + } + + if (wsResult?.status === 'error') { + const wsData = wsResult.data || []; + const hasUrlError = wsData.some(r => r.error && r.error.includes('url not in domain list')); + + if (hasUrlError) { + console.log('2. 🌐 域名配置问题:请在微信公众平台配置WebSocket合法域名'); + console.log(' - 登录 mp.weixin.qq.com'); + console.log(' - 开发 -> 开发管理 -> 开发设置'); + console.log(' - 添加 wss://api.faxianwo.me 到WebSocket合法域名'); + } else { + console.log('2. 🔌 WebSocket连接问题:请检查网络环境和后端服务状态'); + } + } + + console.log('='.repeat(50)); + } +} + +// 创建全局单例 +const wsdiagnostic = new WebSocketDiagnostic(); + +module.exports = wsdiagnostic; diff --git a/utils/websocket-manager-v2.js b/utils/websocket-manager-v2.js new file mode 100644 index 0000000..76ad192 --- /dev/null +++ b/utils/websocket-manager-v2.js @@ -0,0 +1,761 @@ +const config = require('../config/config.js'); + +/** + * 微信小程序专用WebSocket管理器 V2 + * 针对小程序环境优化,解决连接问题 + */ +class WebSocketManagerV2 { + constructor() { + this.ws = null; + this.isConnected = false; + this.isConnecting = false; + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 5; + this.reconnectInterval = 2000; // 2秒 + this.heartbeatInterval = null; + this.heartbeatTimeout = 30000; // 30秒心跳 + this.connectionTimeout = null; + + // 认证信息 + this.token = null; + this.deviceId = null; + + // 事件处理器 + this.messageHandlers = new Map(); + this.eventHandlers = new Map(); + + // 消息队列 + this.messageQueue = []; + this.messageIdCounter = 0; + + // 初始化设备ID + this.initDeviceId(); + + // 绑定方法上下文 + this.onOpen = this.onOpen.bind(this); + this.onMessage = this.onMessage.bind(this); + this.onError = this.onError.bind(this); + this.onClose = this.onClose.bind(this); + } + + // 初始化设备ID + initDeviceId() { + try { + let deviceId = wx.getStorageSync('device_id'); + if (!deviceId) { + deviceId = 'mp_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + wx.setStorageSync('device_id', deviceId); + } + this.deviceId = deviceId; + console.log('🔧 设备ID初始化:', this.deviceId); + } catch (error) { + console.error('❌ 设备ID初始化失败:', error); + this.deviceId = 'mp_unknown_' + Date.now(); + } + } + + // 设置认证token + setToken(token) { + this.token = token; + console.log('🔑 Token已设置:', token ? '成功' : '失败'); + } + + // 获取认证token + getToken() { + if (this.token) { + return this.token; + } + + try { + // 方式1:从app.globalData获取 + const app = getApp(); + if (app?.globalData?.userInfo?.token) { + this.token = app.globalData.userInfo.token; + console.log('📱 从app.globalData获取token成功'); + return this.token; + } + + // 方式2:从本地存储获取userInfo + const userInfo = wx.getStorageSync('userInfo'); + if (userInfo?.token) { + this.token = userInfo.token; + console.log('💾 从userInfo获取token成功'); + return this.token; + } + + // 方式3:直接从本地存储获取token + const directToken = wx.getStorageSync('token'); + if (directToken) { + this.token = directToken; + console.log('💾 从storage直接获取token成功'); + return this.token; + } + + console.warn('⚠️ 未找到有效的token'); + return null; + } catch (error) { + console.error('❌ 获取token失败:', error); + return null; + } + } + + // 连接WebSocket + async connect() { + if (this.isConnected) { + console.log('✅ WebSocket已连接'); + return Promise.resolve(); + } + + if (this.isConnecting) { + console.log('⚠️ WebSocket正在连接中,等待完成...'); + return this.waitForConnection(); + } + + return new Promise((resolve, reject) => { + console.log('🚀 开始连接WebSocket...'); + this.isConnecting = true; + + // 获取token + const token = this.getToken(); + if (!token) { + console.error('❌ 缺少认证token,无法连接WebSocket'); + this.isConnecting = false; + reject(new Error('缺少认证token')); + return; + } + + // 构建WebSocket URL + const wsUrl = config.websocket?.url || 'wss://api.faxianwo.me/api/v1/ws'; + const fullUrl = `${wsUrl}?device_id=${encodeURIComponent(this.deviceId)}`; + + console.log('📡 连接信息:', { + url: fullUrl, + deviceId: this.deviceId, + hasToken: !!token, + tokenLength: token.length + }); + + try { + // 小程序WebSocket连接配置 + const connectOptions = { + url: fullUrl, + header: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + timeout: 15000 + }; + + this.ws = wx.connectSocket(connectOptions); + console.log('📡 WebSocket连接对象已创建'); + + // 设置连接超时 + this.connectionTimeout = setTimeout(() => { + if (this.isConnecting && !this.isConnected) { + console.error('❌ WebSocket连接超时'); + this.cleanup(); + reject(new Error('连接超时')); + } + }, 20000); + + // 事件监听 + this.ws.onOpen((res) => { + console.log('🎉 WebSocket连接成功'); + this.onOpen(res); + resolve(); + }); + + this.ws.onMessage((res) => { + this.onMessage(res); + }); + + this.ws.onError((error) => { + console.error('💥 WebSocket连接错误:', error); + this.onError(error); + if (this.isConnecting) { + reject(error); + } + }); + + this.ws.onClose((res) => { + console.log('🔌 WebSocket连接关闭'); + this.onClose(res); + if (this.isConnecting) { + reject(new Error('连接被关闭')); + } + }); + + } catch (error) { + console.error('❌ WebSocket连接创建失败:', error); + this.cleanup(); + + // 检查域名配置问题 + if (error.errMsg?.includes('url not in domain list')) { + console.error('💡 请在微信公众平台配置WebSocket域名: wss://api.faxianwo.me'); + } + + reject(error); + } + }); + } + + // 等待连接完成 + waitForConnection() { + return new Promise((resolve, reject) => { + const checkConnection = () => { + if (this.isConnected) { + resolve(); + } else if (!this.isConnecting) { + reject(new Error('连接失败')); + } else { + setTimeout(checkConnection, 100); + } + }; + checkConnection(); + }); + } + + // 清理连接状态 + cleanup() { + this.isConnecting = false; + if (this.connectionTimeout) { + clearTimeout(this.connectionTimeout); + this.connectionTimeout = null; + } + } + + // 连接成功处理 + onOpen(res) { + console.log('✅ WebSocket连接建立成功'); + console.log('📊 连接详情:', { + deviceId: this.deviceId, + hasToken: !!this.token, + reconnectAttempts: this.reconnectAttempts, + timestamp: new Date().toLocaleTimeString() + }); + + this.isConnected = true; + this.isConnecting = false; + this.reconnectAttempts = 0; + + this.cleanup(); + this.startHeartbeat(); + this.processMessageQueue(); + this.triggerEvent('connected', res); + } + + // 消息处理 + onMessage(event) { + try { + const message = JSON.parse(event.data); + console.log('📨 收到消息:', message.type || 'unknown'); + + // 心跳响应 + if (message.type === 'pong' || message.type === 'heartbeat_response') { + console.log('💓 心跳响应'); + return; + } + + // 🔥 根据消息类型分发到对应的事件处理器 + switch (message.type) { + case 'new_message': + this.triggerEvent('new_message', message); + break; + case 'message_status': + this.triggerEvent('message_status', message); + break; + case 'message_sent': + // 标准响应:消息发送成功(包括撤回成功) + this.triggerEvent('message_sent', message); + break; + case 'message_recalled': + // 收到其他用户撤回消息的通知 + this.triggerEvent('message_recalled', message); + break; + case 'error': + // 错误响应(包括撤回失败) + this.triggerEvent('error', message); + break; + case 'unread_count_update': + this.triggerEvent('unread_count_update', message); + break; + // 🔥 在线状态相关(presence) + case 'user_online': + this.triggerEvent('user_online', message); + this.triggerEvent('message', message); // 兼容通用监听 + break; + case 'user_offline': + this.triggerEvent('user_offline', message); + this.triggerEvent('message', message); // 兼容通用监听 + break; + case 'presence_update': + this.triggerEvent('presence_update', message); + this.triggerEvent('message', message); // 兼容通用监听 + break; + case 'friend_request': + this.triggerEvent('friend_request', message); + break; + case 'notification': + this.triggerEvent('notification', message); + break; + case 'chat_message': + this.triggerEvent('chat_message', message); + break; + default: + // 尝试查找特定类型的处理器 + const handler = this.messageHandlers.get(message.type); + if (handler) { + handler(message); + } else { + // 触发通用消息事件 + this.triggerEvent('message', message); + } + } + + } catch (error) { + console.error('❌ 消息解析失败:', error); + } + } + + // 错误处理 + onError(error) { + console.error('❌ WebSocket错误:', error); + this.isConnected = false; + this.cleanup(); + this.triggerEvent('error', error); + this.scheduleReconnect(); + } + + // 连接关闭处理 + onClose(event) { + console.log('🔌 WebSocket连接关闭:', event.code); + this.isConnected = false; + this.cleanup(); + this.stopHeartbeat(); + this.triggerEvent('disconnected', event); + + // 非正常关闭时重连 + if (event.code !== 1000) { + this.scheduleReconnect(); + } + } + + // 发送消息 + send(message) { + if (!this.isConnected) { + console.warn('⚠️ WebSocket未连接,消息加入队列'); + this.messageQueue.push(message); + this.connect().catch(console.error); + return false; + } + + try { + const messageStr = typeof message === 'string' ? message : JSON.stringify(message); + this.ws.send({ data: messageStr }); + console.log('📤 消息发送成功'); + return true; + } catch (error) { + console.error('❌ 消息发送失败:', error); + return false; + } + } + + // 处理消息队列 + processMessageQueue() { + while (this.messageQueue.length > 0 && this.isConnected) { + const message = this.messageQueue.shift(); + this.send(message); + } + } + + // 开始心跳 - 使用WebSocket原生ping/pong + startHeartbeat() { + this.stopHeartbeat(); + this.heartbeatInterval = setInterval(() => { + if (this.isConnected && this.ws) { + try { + // 使用微信小程序的ping方法(如果支持) + if (typeof this.ws.ping === 'function') { + this.ws.ping(); + console.log('💓 发送WebSocket原生ping'); + } else { + // 降级:发送应用层心跳(使用正确的格式) + const heartbeatMessage = { + type: 'heartbeat', + id: `heartbeat_${Date.now()}`, + data: { + timestamp: Date.now() + } + }; + this.send(heartbeatMessage); + console.log('💓 发送应用层心跳'); + } + } catch (error) { + console.error('❌ 心跳发送失败:', error); + } + } + }, this.heartbeatTimeout); + } + + // 停止心跳 + stopHeartbeat() { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } + } + + // 计划重连 + scheduleReconnect() { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error('❌ 达到最大重连次数,停止重连'); + return; + } + + const delay = this.reconnectInterval * Math.pow(2, this.reconnectAttempts); + console.log(`🔄 ${delay}ms后尝试第${this.reconnectAttempts + 1}次重连`); + + setTimeout(() => { + this.reconnectAttempts++; + this.connect().catch(console.error); + }, delay); + } + + // 断开连接 + disconnect() { + console.log('🔌 主动断开WebSocket连接'); + this.stopHeartbeat(); + this.cleanup(); + + if (this.ws) { + this.ws.close(); + this.ws = null; + } + + this.isConnected = false; + this.reconnectAttempts = this.maxReconnectAttempts; // 阻止重连 + } + + // 注册消息处理器 + onMessage(type, handler) { + this.messageHandlers.set(type, handler); + } + + // 发送聊天消息 - 符合API文档规范 + sendChatMessage(receiverId, content, msgType = 0, chatType = 0, options = {}) { + // 生成客户端消息ID + const clientMsgId = this.generateMessageId(); + + // 🔥 消息类型映射 - 修正为与API文档一致 + const msgTypeMap = { + 'text': 0, // 文字消息 + 'image': 1, // 图片消息 + 'voice': 2, // 语音消息 + 'video': 3, // 视频消息 + 'file': 4, // 文件消息 + 'emoji': 6, // 表情消息 + 'location': 5 // 位置消息 + }; + + // 🔥 详细记录消息类型转换过程 + console.log('🔍 消息类型转换详情:', { + originalMsgType: msgType, + originalType: typeof msgType, + msgTypeMap: msgTypeMap + }); + + // 如果msgType是字符串,转换为数字 + if (typeof msgType === 'string') { + const convertedMsgType = msgTypeMap[msgType] || 0; + console.log('🔄 字符串转数字:', msgType, '->', convertedMsgType); + msgType = convertedMsgType; + } + + console.log('✅ 最终msgType:', msgType); + + const message = { + type: 'send_message', + id: clientMsgId, + data: { + receiverId: receiverId, + chatType: chatType, + msgType: msgType, + content: content, + atUsers: options.atUsers || [], + replyTo: options.replyTo || '', + extra: options.extra || '' + } + }; + + console.log('📤 发送聊天消息:', { + type: 'send_message', + id: clientMsgId, + receiverId, + chatType, + msgType, + contentLength: content ? content.length : 0, + contentPreview: content ? (content.substring(0, 50) + (content.length > 50 ? '...' : '')) : 'null' + }); + + return this.send(message); + } + + // 生成消息ID + generateMessageId() { + this.messageIdCounter++; + return `client_msg_${Date.now()}_${this.messageIdCounter}`; + } + + // 发送图片消息 + sendImageMessage(receiverId, imageUrl, chatType = 0, options = {}) { + return this.sendChatMessage(receiverId, imageUrl, 1, chatType, options); + } + + // 发送语音消息 + sendVoiceMessage(receiverId, voiceUrl, duration, chatType = 0, options = {}) { + const content = JSON.stringify({ + url: voiceUrl, + duration: duration + }); + return this.sendChatMessage(receiverId, content, 2, chatType, options); + } + + // 发送视频消息 + sendVideoMessage(receiverId, videoUrl, duration, thumbnail, chatType = 0, options = {}) { + const content = JSON.stringify({ + url: videoUrl, + duration: duration, + thumbnail: thumbnail + }); + return this.sendChatMessage(receiverId, content, 3, chatType, options); + } + + // 发送位置消息 + sendLocationMessage(receiverId, latitude, longitude, address, locationName, chatType = 0, options = {}) { + const message = { + type: 'send_location', + id: this.generateMessageId(), + data: { + receiverId: receiverId, + chatType: chatType, + latitude: latitude, + longitude: longitude, + address: address, + locationName: locationName + } + }; + + return this.send(message); + } + + // 撤回消息 + recallMessage(messageId) { + const message = { + type: 'recall_message', + id: this.generateMessageId(), + data: { + messageId: messageId + } + }; + console.log('📤 发送撤回消息请求:', message); + return this.send(message); + } + + // 标记消息已读 + markMessageRead(messageIds) { + const message = { + type: 'mark_read', + id: this.generateMessageId(), + data: { + messageIds: Array.isArray(messageIds) ? messageIds : [messageIds] + } + }; + + return this.send(message); + } + + // 注册事件处理器 + on(event, handler) { + if (!this.eventHandlers.has(event)) { + this.eventHandlers.set(event, []); + } + this.eventHandlers.get(event).push(handler); + } + + // 移除事件处理器 + off(event, handler) { + if (!this.eventHandlers.has(event)) { + return; + } + + const handlers = this.eventHandlers.get(event); + if (handler) { + // 移除特定的处理器 + const index = handlers.indexOf(handler); + if (index > -1) { + handlers.splice(index, 1); + } + } else { + // 移除所有处理器 + this.eventHandlers.set(event, []); + } + + // 如果没有处理器了,删除整个事件 + if (handlers.length === 0) { + this.eventHandlers.delete(event); + } + } + + // 触发事件 + triggerEvent(event, data) { + const handlers = this.eventHandlers.get(event); + if (handlers) { + handlers.forEach(handler => { + try { + handler(data); + } catch (error) { + console.error(`❌ 事件处理器错误 [${event}]:`, error); + } + }); + } + } + + // 获取连接状态 + getStatus() { + return { + isConnected: this.isConnected, + isConnecting: this.isConnecting, + reconnectAttempts: this.reconnectAttempts, + hasToken: !!this.token, + deviceId: this.deviceId + }; + } + + // 兼容性方法 - 为了向后兼容 + getConnectionStatus() { + return { + connected: this.isConnected, + connecting: this.isConnecting + }; + } + + // ===== 聊天功能相关方法 ===== + + // 发送输入状态 + sendTypingStatus(conversationId, isTyping) { + try { + if (!this.isConnected) { + console.warn('⚠️ WebSocket未连接,无法发送输入状态'); + return false; + } + + const message = { + type: 'typing_status', + id: `typing_${Date.now()}`, + data: { + conversationId: conversationId, + isTyping: isTyping, + timestamp: Date.now() + } + }; + + console.log('📝 发送输入状态:', { + conversationId, + isTyping, + timestamp: Date.now() + }); + + return this.send(message); + } catch (error) { + console.error('❌ 发送输入状态失败:', error); + return false; + } + } + + // 发送已读回执 + sendReadReceipt(messageId) { + try { + if (!this.isConnected) { + console.warn('⚠️ WebSocket未连接,无法发送已读回执'); + return false; + } + + const message = { + type: 'mark_read', + id: `read_${Date.now()}`, + data: { + messageId: messageId, + timestamp: Date.now() + } + }; + + console.log('✅ 发送已读回执:', { + messageId, + timestamp: Date.now() + }); + + return this.send(message); + } catch (error) { + console.error('❌ 发送已读回执失败:', error); + return false; + } + } + + // 聊天消息处理器注册 + onChatMessage(handler) { + this.on('new_message', handler); + this.on('chat_message', handler); + } + + // 未读数更新处理器注册 + onUnreadCountUpdate(handler) { + this.on('unread_count_update', handler); + this.on('unread_update', handler); + } + + // 通知处理器注册 + onNotification(handler) { + this.on('notification', handler); + } + + // 好友请求处理器注册 + onFriendRequest(handler) { + this.on('friend_request', handler); + } + + // 发送聊天消息(兼容旧版本接口)- 重定向到标准方法 + sendChatMessageLegacy(targetId, content, messageType = 'text', extra = {}) { + // 转换消息类型 + const msgTypeMap = { + 'text': 1, + 'image': 2, + 'voice': 3, + 'video': 4, + 'file': 5, + 'emoji': 6, + 'location': 7 + }; + + const msgType = msgTypeMap[messageType] || 1; + const chatType = extra.chatType || 0; + + return this.sendChatMessage(targetId, content, msgType, chatType, extra); + } + + // 生成消息ID + generateMessageId() { + return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + // ===== 兼容性别名方法 ===== + + // 兼容旧版本的 sendMessage 方法 + sendMessage(message) { + return this.send(message); + } +} + +// 创建全局实例 +const wsManager = new WebSocketManagerV2(); + +module.exports = wsManager; diff --git a/work.txt b/work.txt new file mode 100644 index 0000000..fd09ff8 --- /dev/null +++ b/work.txt @@ -0,0 +1,41 @@ + + + + {{userInfo.user.gender === 'male' ? '♂️' : userInfo.user.gender === 'female' ? '♀️' : '?'}} + + + + 年龄 + {{userInfo.age}} + + + + 心情 + {{userInfo.mood}} + + + + 人格 + {{userInfo.personality}} + + + + 身份 + {{userInfo.identity}} + + + + 星座 + {{userInfo.constellation}} + + + + 学校 + {{userInfo.school}} + + + + 职业 + {{userInfo.occupation}} + + \ No newline at end of file