From 1d71a02738b2a74c210033c103d6a7b0689b0e9b Mon Sep 17 00:00:00 2001 From: Rajuahamedkst Date: Fri, 12 Sep 2025 16:08:17 +0800 Subject: [PATCH] Initial Commit --- .eslintrc.js | 31 + .gitignore | 14 + .vscode/settings.json | 8 + README.md | 0 app.js | 507 +++ app.json | 60 + app.wxss | 55 + .../interactive-feedback.js | 567 +++ .../interactive-feedback.json | 4 + .../interactive-feedback.wxml | 44 + .../interactive-feedback.wxss | 446 +++ components/media-preview/media-preview.js | 555 +++ components/media-preview/media-preview.json | 4 + components/media-preview/media-preview.wxml | 192 + components/media-preview/media-preview.wxss | 586 ++++ .../mention-selector/mention-selector.js | 181 + .../mention-selector/mention-selector.json | 4 + .../mention-selector/mention-selector.wxml | 85 + .../mention-selector/mention-selector.wxss | 378 ++ .../message-action-menu.js | 546 +++ .../message-action-menu.json | 4 + .../message-action-menu.wxml | 191 + .../message-action-menu.wxss | 446 +++ components/navigation-bar/navigation-bar.js | 118 + components/navigation-bar/navigation-bar.json | 5 + components/navigation-bar/navigation-bar.wxml | 64 + components/navigation-bar/navigation-bar.wxss | 96 + components/page-transition/page-transition.js | 456 +++ .../page-transition/page-transition.json | 4 + .../page-transition/page-transition.wxml | 20 + .../page-transition/page-transition.wxss | 386 +++ components/voice-message/voice-message.js | 321 ++ components/voice-message/voice-message.json | 4 + components/voice-message/voice-message.wxml | 53 + components/voice-message/voice-message.wxss | 443 +++ components/voice-recorder/voice-recorder.js | 562 +++ components/voice-recorder/voice-recorder.json | 4 + components/voice-recorder/voice-recorder.wxml | 150 + components/voice-recorder/voice-recorder.wxss | 534 +++ config/config.js | 164 + images/1111.jpg | Bin 0 -> 176116 bytes images/Edit3.png | Bin 0 -> 295 bytes images/Group.png | Bin 0 -> 267 bytes images/Icon.png | Bin 0 -> 734 bytes images/QR_code.png | Bin 0 -> 407 bytes images/Subtract.png | Bin 0 -> 432 bytes images/about.png | Bin 0 -> 199 bytes images/album-bold.png | Bin 0 -> 663 bytes images/btn.png | Bin 0 -> 525 bytes images/camera.png | Bin 0 -> 13061 bytes images/cameras.png | Bin 0 -> 20364 bytes images/chat.png | Bin 0 -> 214 bytes images/download.svg | 1 + images/emoji-lol.png | Bin 0 -> 711 bytes images/eye.png | Bin 0 -> 229 bytes images/findme-logo.png | Bin 0 -> 7160 bytes images/folder-favorite.png | Bin 0 -> 465 bytes images/game-fill.png | Bin 0 -> 613 bytes images/lang.png | Bin 0 -> 237 bytes images/location.png | Bin 0 -> 339 bytes images/logo.png | Bin 0 -> 7160 bytes images/map/marker_blank.png | Bin 0 -> 131 bytes images/map/marker_canguan.png | Bin 0 -> 8544 bytes images/map/marker_hong.png | Bin 0 -> 7091 bytes images/map/marker_kafei.png | Bin 0 -> 5258 bytes images/map/marker_lan.png | Bin 0 -> 7126 bytes images/map/marker_yinliao.png | Bin 0 -> 4510 bytes images/map/marker_yule.png | Bin 0 -> 4821 bytes images/moments.png | Bin 0 -> 551865 bytes images/news.png | Bin 0 -> 208 bytes images/placeholder.txt | 8 + images/qr-code.png | Bin 0 -> 461 bytes images/return.png | Bin 0 -> 153 bytes images/scan.svg | 1 + images/share.svg | 1 + images/shopping-bag.png | Bin 0 -> 442 bytes images/subject.png | Bin 0 -> 236 bytes images/tag.png | Bin 0 -> 701 bytes images/view.png | Bin 0 -> 189 bytes images/vip-card1.png | Bin 0 -> 16988 bytes images/vip-card2.png | Bin 0 -> 25081 bytes images/vip-card3.png | Bin 0 -> 27042 bytes images/wallet-solid.png | Bin 0 -> 398 bytes libs/amap-wx-all.js | 31 + libs/amap-wx.js | 114 + minitest/test.config.json | 3 + .../phone-binding/phone-binding.js | 490 +++ .../phone-binding/phone-binding.json | 5 + .../phone-binding/phone-binding.wxml | 141 + .../phone-binding/phone-binding.wxss | 384 ++ pages/chat-settings/chat-settings.js | 469 +++ pages/chat-settings/chat-settings.json | 7 + pages/chat-settings/chat-settings.wxml | 217 ++ pages/chat-settings/chat-settings.wxss | 529 +++ pages/edit/edit.js | 622 ++++ pages/edit/edit.json | 7 + pages/edit/edit.wxml | 310 ++ pages/edit/edit.wxss | 651 ++++ pages/group/create-group/create-group.js | 374 ++ pages/group/create-group/create-group.json | 7 + pages/group/create-group/create-group.wxml | 191 + pages/group/create-group/create-group.wxss | 639 ++++ .../group-announcement/group-announcement.js | 430 +++ .../group-announcement.json | 7 + .../group-announcement.wxml | 174 + .../group-announcement.wxss | 663 ++++ pages/group/group-info/group-info.js | 966 ++++++ pages/group/group-info/group-info.json | 7 + pages/group/group-info/group-info.wxml | 317 ++ pages/group/group-info/group-info.wxss | 829 +++++ pages/group/group-members/group-members.js | 433 +++ pages/group/group-members/group-members.json | 7 + pages/group/group-members/group-members.wxml | 243 ++ pages/group/group-members/group-members.wxss | 615 ++++ pages/login/login.js | 652 ++++ pages/login/login.json | 7 + pages/login/login.wxml | 154 + pages/login/login.wxss | 660 ++++ pages/map/map.js | 2060 +++++++++++ pages/map/map.json | 8 + pages/map/map.wxml | 478 +++ pages/map/map.wxss | 2734 +++++++++++++++ pages/message/chat/chat.js | 3085 +++++++++++++++++ pages/message/chat/chat.json | 17 + pages/message/chat/chat.wxml | 412 +++ pages/message/chat/chat.wxss | 1020 ++++++ pages/message/message.js | 1399 ++++++++ pages/message/message.json | 18 + pages/message/message.wxml | 108 + pages/message/message.wxss | 515 +++ pages/personal-details/personal-details.js | 339 ++ pages/personal-details/personal-details.json | 7 + pages/personal-details/personal-details.wxml | 159 + pages/personal-details/personal-details.wxss | 247 ++ pages/profile/profile.js | 805 +++++ pages/profile/profile.json | 5 + pages/profile/profile.wxml | 305 ++ pages/profile/profile.wxss | 861 +++++ pages/qr-code/qr-code.js | 484 +++ pages/qr-code/qr-code.json | 8 + pages/qr-code/qr-code.wxml | 92 + pages/qr-code/qr-code.wxss | 167 + pages/search/global-search.js | 457 +++ pages/search/global-search.json | 7 + pages/search/global-search.wxml | 227 ++ pages/search/global-search.wxss | 712 ++++ pages/settings/about/about.js | 88 + pages/settings/about/about.json | 4 + pages/settings/about/about.wxml | 38 + pages/settings/about/about.wxss | 122 + pages/settings/about/update-log/update-log.js | 176 + .../settings/about/update-log/update-log.json | 7 + .../settings/about/update-log/update-log.wxml | 92 + .../settings/about/update-log/update-log.wxss | 326 ++ .../account-security/account-security.js | 115 + .../account-security/account-security.json | 8 + .../account-security/account-security.wxml | 63 + .../account-security/account-security.wxss | 159 + pages/settings/feedback/feedback.js | 109 + pages/settings/feedback/feedback.json | 8 + pages/settings/feedback/feedback.wxml | 56 + pages/settings/feedback/feedback.wxss | 169 + .../notification-settings.js | 592 ++++ .../notification-settings.json | 7 + .../notification-settings.wxml | 312 ++ .../notification-settings.wxss | 582 ++++ pages/settingss/settingss.js | 266 ++ pages/settingss/settingss.json | 9 + pages/settingss/settingss.wxml | 136 + pages/settingss/settingss.wxss | 190 + pages/social/friend-detail/friend-detail.js | 253 ++ pages/social/friend-detail/friend-detail.json | 4 + pages/social/friend-detail/friend-detail.wxml | 178 + pages/social/friend-detail/friend-detail.wxss | 396 +++ .../social/friend-requests/friend-requests.js | 248 ++ .../friend-requests/friend-requests.wxml | 190 + .../friend-requests/friend-requests.wxss | 353 ++ pages/social/friends/friends.js | 959 +++++ pages/social/friends/friends.json | 7 + pages/social/friends/friends.wxml | 125 + pages/social/friends/friends.wxss | 431 +++ pages/social/search/search.js | 291 ++ pages/social/search/search.wxml | 168 + pages/social/search/search.wxss | 470 +++ pages/splash/splash.js | 283 ++ pages/splash/splash.json | 7 + pages/splash/splash.wxml | 92 + pages/splash/splash.wxss | 629 ++++ pages/websocket-test/websocket-test.js | 317 ++ pages/websocket-test/websocket-test.json | 8 + pages/websocket-test/websocket-test.wxml | 110 + pages/websocket-test/websocket-test.wxss | 251 ++ project.config.json | 42 + project.private.config.json | 24 + sitemap.json | 7 + styles/components.wxss | 503 +++ styles/design-system.wxss | 251 ++ styles/responsive.wxss | 403 +++ styles/screen-adaption.wxss | 222 ++ test-feedback.js | 35 + test-friends-layout.html | 323 ++ test.js | 393 +++ utils/account-sync.js | 247 ++ utils/animation-manager.js | 564 +++ utils/api-client.js | 1091 ++++++ utils/auth.js | 446 +++ utils/chat-api.js | 326 ++ utils/chat-message-handler.js | 300 ++ utils/device.js | 475 +++ utils/error-handler.js | 692 ++++ utils/friend-api.js | 324 ++ utils/group-api.js | 331 ++ utils/group-chat-manager.js | 699 ++++ utils/image-cache-manager.js | 283 ++ utils/map-config.js | 336 ++ utils/map-setup.md | 118 + utils/media-manager.js | 833 +++++ utils/media-picker.js | 366 ++ utils/message-history-manager.js | 671 ++++ utils/message-interaction-manager.js | 725 ++++ utils/message-search-manager.js | 484 +++ utils/message-sync-manager.js | 614 ++++ utils/network-optimizer.js | 684 ++++ utils/notification-manager.js | 592 ++++ utils/performance-monitor.js | 768 ++++ utils/performance-optimizer.js | 325 ++ utils/screen-adapter.js | 354 ++ utils/storage.js | 294 ++ utils/subscribe-message-manager.js | 440 +++ utils/system-info-helper.js | 247 ++ utils/system-info-modern.js | 241 ++ utils/system-info.js | 129 + utils/ui-helper.js | 365 ++ utils/voice-message-manager.js | 751 ++++ utils/websocket-diagnostic.js | 375 ++ utils/websocket-manager-v2.js | 761 ++++ work.txt | 41 + 237 files changed, 64293 insertions(+) create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 app.js create mode 100644 app.json create mode 100644 app.wxss create mode 100644 components/interactive-feedback/interactive-feedback.js create mode 100644 components/interactive-feedback/interactive-feedback.json create mode 100644 components/interactive-feedback/interactive-feedback.wxml create mode 100644 components/interactive-feedback/interactive-feedback.wxss create mode 100644 components/media-preview/media-preview.js create mode 100644 components/media-preview/media-preview.json create mode 100644 components/media-preview/media-preview.wxml create mode 100644 components/media-preview/media-preview.wxss create mode 100644 components/mention-selector/mention-selector.js create mode 100644 components/mention-selector/mention-selector.json create mode 100644 components/mention-selector/mention-selector.wxml create mode 100644 components/mention-selector/mention-selector.wxss create mode 100644 components/message-action-menu/message-action-menu.js create mode 100644 components/message-action-menu/message-action-menu.json create mode 100644 components/message-action-menu/message-action-menu.wxml create mode 100644 components/message-action-menu/message-action-menu.wxss create mode 100644 components/navigation-bar/navigation-bar.js create mode 100644 components/navigation-bar/navigation-bar.json create mode 100644 components/navigation-bar/navigation-bar.wxml create mode 100644 components/navigation-bar/navigation-bar.wxss create mode 100644 components/page-transition/page-transition.js create mode 100644 components/page-transition/page-transition.json create mode 100644 components/page-transition/page-transition.wxml create mode 100644 components/page-transition/page-transition.wxss create mode 100644 components/voice-message/voice-message.js create mode 100644 components/voice-message/voice-message.json create mode 100644 components/voice-message/voice-message.wxml create mode 100644 components/voice-message/voice-message.wxss create mode 100644 components/voice-recorder/voice-recorder.js create mode 100644 components/voice-recorder/voice-recorder.json create mode 100644 components/voice-recorder/voice-recorder.wxml create mode 100644 components/voice-recorder/voice-recorder.wxss create mode 100644 config/config.js create mode 100644 images/1111.jpg create mode 100644 images/Edit3.png create mode 100644 images/Group.png create mode 100644 images/Icon.png create mode 100644 images/QR_code.png create mode 100644 images/Subtract.png create mode 100644 images/about.png create mode 100644 images/album-bold.png create mode 100644 images/btn.png create mode 100644 images/camera.png create mode 100644 images/cameras.png create mode 100644 images/chat.png create mode 100644 images/download.svg create mode 100644 images/emoji-lol.png create mode 100644 images/eye.png create mode 100644 images/findme-logo.png create mode 100644 images/folder-favorite.png create mode 100644 images/game-fill.png create mode 100644 images/lang.png create mode 100644 images/location.png create mode 100644 images/logo.png create mode 100644 images/map/marker_blank.png create mode 100644 images/map/marker_canguan.png create mode 100644 images/map/marker_hong.png create mode 100644 images/map/marker_kafei.png create mode 100644 images/map/marker_lan.png create mode 100644 images/map/marker_yinliao.png create mode 100644 images/map/marker_yule.png create mode 100644 images/moments.png create mode 100644 images/news.png create mode 100644 images/placeholder.txt create mode 100644 images/qr-code.png create mode 100644 images/return.png create mode 100644 images/scan.svg create mode 100644 images/share.svg create mode 100644 images/shopping-bag.png create mode 100644 images/subject.png create mode 100644 images/tag.png create mode 100644 images/view.png create mode 100644 images/vip-card1.png create mode 100644 images/vip-card2.png create mode 100644 images/vip-card3.png create mode 100644 images/wallet-solid.png create mode 100644 libs/amap-wx-all.js create mode 100644 libs/amap-wx.js create mode 100644 minitest/test.config.json create mode 100644 pages/account-sync/phone-binding/phone-binding.js create mode 100644 pages/account-sync/phone-binding/phone-binding.json create mode 100644 pages/account-sync/phone-binding/phone-binding.wxml create mode 100644 pages/account-sync/phone-binding/phone-binding.wxss create mode 100644 pages/chat-settings/chat-settings.js create mode 100644 pages/chat-settings/chat-settings.json create mode 100644 pages/chat-settings/chat-settings.wxml create mode 100644 pages/chat-settings/chat-settings.wxss create mode 100644 pages/edit/edit.js create mode 100644 pages/edit/edit.json create mode 100644 pages/edit/edit.wxml create mode 100644 pages/edit/edit.wxss create mode 100644 pages/group/create-group/create-group.js create mode 100644 pages/group/create-group/create-group.json create mode 100644 pages/group/create-group/create-group.wxml create mode 100644 pages/group/create-group/create-group.wxss create mode 100644 pages/group/group-announcement/group-announcement.js create mode 100644 pages/group/group-announcement/group-announcement.json create mode 100644 pages/group/group-announcement/group-announcement.wxml create mode 100644 pages/group/group-announcement/group-announcement.wxss create mode 100644 pages/group/group-info/group-info.js create mode 100644 pages/group/group-info/group-info.json create mode 100644 pages/group/group-info/group-info.wxml create mode 100644 pages/group/group-info/group-info.wxss create mode 100644 pages/group/group-members/group-members.js create mode 100644 pages/group/group-members/group-members.json create mode 100644 pages/group/group-members/group-members.wxml create mode 100644 pages/group/group-members/group-members.wxss create mode 100644 pages/login/login.js create mode 100644 pages/login/login.json create mode 100644 pages/login/login.wxml create mode 100644 pages/login/login.wxss create mode 100644 pages/map/map.js create mode 100644 pages/map/map.json create mode 100644 pages/map/map.wxml create mode 100644 pages/map/map.wxss create mode 100644 pages/message/chat/chat.js create mode 100644 pages/message/chat/chat.json create mode 100644 pages/message/chat/chat.wxml create mode 100644 pages/message/chat/chat.wxss create mode 100644 pages/message/message.js create mode 100644 pages/message/message.json create mode 100644 pages/message/message.wxml create mode 100644 pages/message/message.wxss create mode 100644 pages/personal-details/personal-details.js create mode 100644 pages/personal-details/personal-details.json create mode 100644 pages/personal-details/personal-details.wxml create mode 100644 pages/personal-details/personal-details.wxss create mode 100644 pages/profile/profile.js create mode 100644 pages/profile/profile.json create mode 100644 pages/profile/profile.wxml create mode 100644 pages/profile/profile.wxss create mode 100644 pages/qr-code/qr-code.js create mode 100644 pages/qr-code/qr-code.json create mode 100644 pages/qr-code/qr-code.wxml create mode 100644 pages/qr-code/qr-code.wxss create mode 100644 pages/search/global-search.js create mode 100644 pages/search/global-search.json create mode 100644 pages/search/global-search.wxml create mode 100644 pages/search/global-search.wxss create mode 100644 pages/settings/about/about.js create mode 100644 pages/settings/about/about.json create mode 100644 pages/settings/about/about.wxml create mode 100644 pages/settings/about/about.wxss create mode 100644 pages/settings/about/update-log/update-log.js create mode 100644 pages/settings/about/update-log/update-log.json create mode 100644 pages/settings/about/update-log/update-log.wxml create mode 100644 pages/settings/about/update-log/update-log.wxss create mode 100644 pages/settings/account-security/account-security.js create mode 100644 pages/settings/account-security/account-security.json create mode 100644 pages/settings/account-security/account-security.wxml create mode 100644 pages/settings/account-security/account-security.wxss create mode 100644 pages/settings/feedback/feedback.js create mode 100644 pages/settings/feedback/feedback.json create mode 100644 pages/settings/feedback/feedback.wxml create mode 100644 pages/settings/feedback/feedback.wxss create mode 100644 pages/settings/notification-settings/notification-settings.js create mode 100644 pages/settings/notification-settings/notification-settings.json create mode 100644 pages/settings/notification-settings/notification-settings.wxml create mode 100644 pages/settings/notification-settings/notification-settings.wxss create mode 100644 pages/settingss/settingss.js create mode 100644 pages/settingss/settingss.json create mode 100644 pages/settingss/settingss.wxml create mode 100644 pages/settingss/settingss.wxss create mode 100644 pages/social/friend-detail/friend-detail.js create mode 100644 pages/social/friend-detail/friend-detail.json create mode 100644 pages/social/friend-detail/friend-detail.wxml create mode 100644 pages/social/friend-detail/friend-detail.wxss create mode 100644 pages/social/friend-requests/friend-requests.js create mode 100644 pages/social/friend-requests/friend-requests.wxml create mode 100644 pages/social/friend-requests/friend-requests.wxss create mode 100644 pages/social/friends/friends.js create mode 100644 pages/social/friends/friends.json create mode 100644 pages/social/friends/friends.wxml create mode 100644 pages/social/friends/friends.wxss create mode 100644 pages/social/search/search.js create mode 100644 pages/social/search/search.wxml create mode 100644 pages/social/search/search.wxss create mode 100644 pages/splash/splash.js create mode 100644 pages/splash/splash.json create mode 100644 pages/splash/splash.wxml create mode 100644 pages/splash/splash.wxss create mode 100644 pages/websocket-test/websocket-test.js create mode 100644 pages/websocket-test/websocket-test.json create mode 100644 pages/websocket-test/websocket-test.wxml create mode 100644 pages/websocket-test/websocket-test.wxss create mode 100644 project.config.json create mode 100644 project.private.config.json create mode 100644 sitemap.json create mode 100644 styles/components.wxss create mode 100644 styles/design-system.wxss create mode 100644 styles/responsive.wxss create mode 100644 styles/screen-adaption.wxss create mode 100644 test-feedback.js create mode 100644 test-friends-layout.html create mode 100644 test.js create mode 100644 utils/account-sync.js create mode 100644 utils/animation-manager.js create mode 100644 utils/api-client.js create mode 100644 utils/auth.js create mode 100644 utils/chat-api.js create mode 100644 utils/chat-message-handler.js create mode 100644 utils/device.js create mode 100644 utils/error-handler.js create mode 100644 utils/friend-api.js create mode 100644 utils/group-api.js create mode 100644 utils/group-chat-manager.js create mode 100644 utils/image-cache-manager.js create mode 100644 utils/map-config.js create mode 100644 utils/map-setup.md create mode 100644 utils/media-manager.js create mode 100644 utils/media-picker.js create mode 100644 utils/message-history-manager.js create mode 100644 utils/message-interaction-manager.js create mode 100644 utils/message-search-manager.js create mode 100644 utils/message-sync-manager.js create mode 100644 utils/network-optimizer.js create mode 100644 utils/notification-manager.js create mode 100644 utils/performance-monitor.js create mode 100644 utils/performance-optimizer.js create mode 100644 utils/screen-adapter.js create mode 100644 utils/storage.js create mode 100644 utils/subscribe-message-manager.js create mode 100644 utils/system-info-helper.js create mode 100644 utils/system-info-modern.js create mode 100644 utils/system-info.js create mode 100644 utils/ui-helper.js create mode 100644 utils/voice-message-manager.js create mode 100644 utils/websocket-diagnostic.js create mode 100644 utils/websocket-manager-v2.js create mode 100644 work.txt 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 0000000000000000000000000000000000000000..60d5cdb46238b4aa56dbe34eabd27655e2ec36dc GIT binary patch literal 176116 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 GAO0WpBLeaO literal 0 HcmV?d00001 diff --git a/images/Edit3.png b/images/Edit3.png new file mode 100644 index 0000000000000000000000000000000000000000..ce9bb3f3ef778171f5f6d2498243ecb5bd815dee GIT binary patch literal 295 zcmeAS@N?(olHy`uVBq!ia0vp^fi+27srqa#=c$%Zrh~YTf z21aQGwFFi(2cH8RYZ!zbJlU0hh*8o|0J>k`3pQ<978G?OZyoG4;k>dO0!Q6ka^JXh=DzX ziEjpT!J&Wy0R}QYOhTvr?~d2Iox8Jo`nI1xeRVXvG literal 0 HcmV?d00001 diff --git a/images/Icon.png b/images/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2bc4096864dd127f967d88b44d8f08461d40fbf8 GIT binary patch literal 734 zcmV<40wMj0P)eR*#iJU<5uPL`{lrrtx^tvJo)C+o^eBoQ4OY0M>+*bv6HCNk0^@6rAx>l+#ut34 zpqIuekY)Ftm)wKv2B!n0U#2~#Grr~k?Zmt7pLd>tKQO)VLIG`y6>9`c>L|>M2ixxL zioi4GhcybV2+Z{9*R-Nwi-0rVBC}yxu-vn}299~`yo1JB^H-L6tAKH0+r?4iuA!vF z|08fJesUVDEPvmjoBG&(qM1S^(&92+_<}IpX+&|4$kdfKXY($5=y~-yuVyp1h0-~o-DZ)6V8 zod~B5zRny>*rswq=KSvKiw0vhBt_#9)S2$<#K-->apr^^HP)04(Ve=jbBabq@uR literal 0 HcmV?d00001 diff --git a/images/QR_code.png b/images/QR_code.png new file mode 100644 index 0000000000000000000000000000000000000000..71c13a48abd7887d19924612c3ac802f9d3cec1e GIT binary patch literal 407 zcmV;I0cie-P)H%OfR;u#$35a6S z)e`HvyrnrSa-R6#jF^e!do^>U@)i0M) z>$8S{y)xp!y7k6sL6hbb=8-I&f>J;0G|4uL;sXM3ia*S_(DDEP002ovPDHLkV1hWF BvycD) literal 0 HcmV?d00001 diff --git a/images/Subtract.png b/images/Subtract.png new file mode 100644 index 0000000000000000000000000000000000000000..7fd5f18bdb35cf8694d6308c157892e2ae8e909a GIT binary patch literal 432 zcmV;h0Z;ykP)stG&I| z>1^+eQP=gW_x?p=(=_eIaeONAgR{ro2|0utiyV}L2Rv0fHeXK z!J@o7R~|?E5Z~aqv6Adxif9QG_oa?8h`qKDFftpS+hG_y-uJ{x!qC|IrfI(2sSAp% zVfmmz1gzj-&uPYw0RfMg^cn^BoQurrz=W^4k(f^aobA&Qc@$?1arG-AJ;c|X2a9Y8 zX{Qie=9OIiyE`s8=g~;ARm+Ywo{x6TV(&_c8IZD5fSqiJuvVXQQ4LoY4c;1@Z-Ok| z^oBF)W9-lEuw1ir_{i@@TrTa9XBR&ATC6{~t`aE$D(D9_$v>Sx#b*;QjePEZbSPlG aMg9k{v$373{uN9B00001|%O$WD@{!3Opi<85meQL6|W&W9~1YAY*Zm zyA#8@b22Z19F}xPUq=Rpjs4tz5?O)#7*7|+kP61$35}cw6nI$f`#tcuyC(8zr;K9W zB(=4z>LMQ>w6<_(F&sa$>iMS?dta>4_IUBlzy65j!8t3%E{NKk&FB<%DJ**bfoF}R qqUpOMvnO5oR_C5QVT=0D7HQ3!C#LFzt!f0?!{F)a=d#Wzp$P!&okhd| literal 0 HcmV?d00001 diff --git a/images/album-bold.png b/images/album-bold.png new file mode 100644 index 0000000000000000000000000000000000000000..547aaf0f419948e06a9545c029cd385db23a64f1 GIT binary patch literal 663 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%0A2{a=E{g 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 CoEAj@ literal 0 HcmV?d00001 diff --git a/images/chat.png b/images/chat.png new file mode 100644 index 0000000000000000000000000000000000000000..9a8b5febff2cc92b46e1a185b2233613fb16574d GIT binary patch literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqoCO|{#S9FrogmDZn=$tnP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBezvEJV@L&KYhNI5g8`3AJ}1+Jh89l;CJhIs zh=Yt84D1mH4yrJ4ozm6xW>(z&C$XmusthQ293(pXv*{#(Y($=d$J \ No newline at end of file diff --git a/images/emoji-lol.png b/images/emoji-lol.png new file mode 100644 index 0000000000000000000000000000000000000000..0bb3e963e6d742b4d4a56d78b620c3604cb315c0 GIT binary patch literal 711 zcmV;&0yzDNP)vq5izW&`L1WrK7CIziG2S|&Jmf`kc#loJnQET8QIKJfUx zr+0rW%R1SX?F0DWozOueM1*`${~})Lb_s1r{_=W{aUn;h zIB!04OurtX0Zpc#)Xo4iNDcn)@J7%$>!tfQaak8BB41j*R3deV-Z{PcOg#+lFT&+( zX&9^Jyr6Lp`61$9{Jm( zLdG$RTQ>YFd_m*li%{s4J|8l=7(OhZj8mOG4$AS7Q&T)~x-??FiU-Y8jZU4EIu=Ll zKg`F29jVWmdUs|Cjl0t+29>>0hK6mNu)7ZRa|!lg?{0z7vxm%>((K-Y%|-Tnzy_{M z201<5BAby?T{jV}S?j9hScy~n3CDVPDd=∓0ai^iP^58Lg#Z6!{`!LYAn9DK0zB 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 literal 0 HcmV?d00001 diff --git a/images/folder-favorite.png b/images/folder-favorite.png new file mode 100644 index 0000000000000000000000000000000000000000..3b8ff74c9f6848f70391e2163b035f9cfefb3a68 GIT binary patch literal 465 zcmV;?0WSWDP)bmWkK{_A zAGu3XfJ`QHS*YO!Yi&u*hP1*w2hxL5>I0IHskIH~ljQRw&N5cpIFg%F17j^U8gX!1 zV^V5J6_ta5kg;>Qb8Gbqm&1!2NE-`D?X4R~8+#%(h~OG>7^<0u>X!Q@0+F1tieK0q zjw>$+DPufaZ%({JASqJWkoGw4V=RNg43WuKN+~adqLc304Se8z%!b3e{u%+$_~MUK zrcG_^#IvUDUX0RN_4Ti=lEJyF4ANya)$BV}Qz$nZ@}6ZqmA;YAem800000NkvXX Hu0mjfHVVYH literal 0 HcmV?d00001 diff --git a/images/game-fill.png b/images/game-fill.png new file mode 100644 index 0000000000000000000000000000000000000000..4ec4062e0551a64818e00d1a4c7c038d90d4bdab GIT binary patch literal 613 zcmV-r0-F7aP)NJ+6iETq!YjfrW2SARI4$ZD{RMxPboAB6_Mk*tV zfbA46!8n*<8q|1gWP_)HgqXoO&`4x7at`EMgxY_I#I}Gor>+J0US?Tfn;-v$%n3|I z9BKPKb(&Sj_c}EwiQ#~zo{^0OSXsR>#*rz3Ed$2m%#L8jEUdbMDg%aVuE(BhM_1cm z1=A7sy>Emy_sLgfsPJu_21rM<5&U-Uzo@z!*|`qr*;f5ekN=Xl%>#P@KW=XtBd=^* zdUWoec<&jL!29rSfZ;^ep~mbKaTK!EhHdV~7K#o9)wObJJk1-*$xXzjbo5fn%P~Dh zPCF9f(D&LHH_+x3O{JT^W@s~iUyIej`l z1;*Z~G0RJrGHCbr|T>aSwP!V{dfJr-~w*8d0kf9Zs) zTLc_PXp0@KG&-*Ap=WFxRHF91{R9sT3|x^Pds(M~>JoMB00000NkvXXu0mjfd$I`( literal 0 HcmV?d00001 diff --git a/images/lang.png b/images/lang.png new file mode 100644 index 0000000000000000000000000000000000000000..5813b77a6d4ed69517210fa8dd1c524ee5d85590 GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4aTa()7Beugc7iZtZpPeSKtaah zAa^H*b?0PW0y!+{j=qiz3>*8o|0J>k`AwcKjv*C{wG#q)8w>=T*&Pq4C?xVpa5)^3 zX<$)lU{Yb=Qe))wa5(fr^{+(B+LPXT*O%>_6BNR+kbSim=QGKBw?h3mdJ_D8iuxSi z$>_fBWK;Kx4EqjocJl&bjqg`j4OfLWc literal 0 HcmV?d00001 diff --git a/images/location.png b/images/location.png new file mode 100644 index 0000000000000000000000000000000000000000..32349e656b7ed42574720a9a9918e401e87349bd GIT binary patch literal 339 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 literal 0 HcmV?d00001 diff --git a/images/map/marker_blank.png b/images/map/marker_blank.png new file mode 100644 index 0000000000000000000000000000000000000000..da6fe0b64e3deaba4efa69fcd6d518ea16099131 GIT binary patch literal 131 zcmeAS@N?(olHy`uVBq!ia0vp^Od!n23?w}&=BEQG#^NA%C&rs6b?Si}&H|6fVg?3o zVGw3ym^DWND99Y(6XFV_msi=|0%<4-@(X5gcy=QV$l>vHaSW-Llbn)}0_1TqFz;4j R%mK0(JYD@<);T3K0RS7E8nplb literal 0 HcmV?d00001 diff --git a/images/map/marker_canguan.png b/images/map/marker_canguan.png new file mode 100644 index 0000000000000000000000000000000000000000..2c31bd013933652ad6b813ff12c6350083f79015 GIT binary patch literal 8544 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 W2#A}Xe1R*Oz>}9%k|~q=fc_t!AwSCi literal 0 HcmV?d00001 diff --git a/images/map/marker_hong.png b/images/map/marker_hong.png new file mode 100644 index 0000000000000000000000000000000000000000..f53aa34dbb4e72c632100c0c05de124a64842cfb GIT binary patch literal 7091 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 literal 0 HcmV?d00001 diff --git a/images/map/marker_kafei.png b/images/map/marker_kafei.png new file mode 100644 index 0000000000000000000000000000000000000000..cb91b6d85fc986899ddf1145d65607023b035a87 GIT binary patch literal 5258 zcmb`L=Tp;9xWy>}LQg`IB77kMq)6{5B-8`}siBD!BT}S=rUC+?cS3JU5$U~1RU)9m zPeeiKT|oi}h#*M6+&|&Y+!wpg?9QAsyD#?HIiGkFV|``@9tH{u3T6yi$Lz8W{_g~Ulpv5D3lAHNEyH*UTI8hNludr-(^iQ5^C)i`_$-j<+_?BQkhv%b2FGmfp5cvm0^l_#zd)2N+gXm%7U0EHL3I< z87$h4w3P6;_*g+lkSwZ|1P2si!I zlO6wx$X`7Zd!ol+NSo=g-6T*=GX3u2F}Ua6S3j_Sf$sl3#1K?N|7>COPWNp+*I3jlHtZA#5oph=o#PvfJGmAjl8Qlm)GyfK*UEK9eE$4 z)w}3GyYcnp*O^nuZY(YeISN`2VXZ)=6~!U=_uP-w>7LA86d$5RT@3rGHcB<%i|ul2 z;BQFCTG&6M+DRB?-M{eofayP9&(&YAf=tlvSqDFk9m6yQO%zabGBhF|t);BfuYo$D zE^!DcIlX;Lpwc}||97%@BIoV27(_g%UT{dDt0)e46z!(ZdgAT>5mQk8*~+MKndmWl zo&qs)5*FOe)WYWjn~s#mLj1U?^wQ7-+nXr0 zA}5U!(_4^*VKZpi=HA1UJiSfb^s3VQs^kbId`+$Vo1yNlF1*h;3?y5}V;CLG z;PW&)<`HtBZnH(avN*G(w8jzx$>r19V-V55b%O|@E}6%ga12P;_EFPWT}3&f*xXsa z)sBH>aSR6p0CBhYl-=RAI3W&X)ClY6sp1eF<)>NkD;%)6sw8!HEQ`7}1`tBARww-$j){67x< zF~e84-l+We4*`Dgk*;>&*QZSbK^OW`#KL!&RtWc>udR~0`z02{GP`mPI$GNLinYwQX< zlSW3nmLK3f!@P3Fj#32?itVOSs1PbHV1OBQdYixU!18_HS~89v-|5czll`7_HV`S3 zS`;Rd@Fx?m4?FDIight_){waQw0Y*HM5t~ru>xiWj%`~npLiM*F1Vyi0z3CPfro+1 zhPf+q*#t$XW5QV|zXED=wOC=Wj_@FvzS&@@#YqXwN7n2ruYkSqd<%lU`S<{4sYG>K zYQ;#s>uXO%I$Qw-iWsq9QE}ficLzOBX#i{Tef8Ql3%eF@;!e1tYIfU*a*NL8`bv0s z0-%?|SMCK51rG*)bc#|VEKRPj$mnhMcY#4`m37mhvM9sgT2=dkvjZ3NzpJ3-kznsY zmbxE95O4=IU{gejx!iN`TGg1>QR&b+ID7~B_oy&cIica^Xv9q8c7#{TMZol@H;XQx zBvnz){{(1sl-a4WEICF!tk-UaarDuQY!pFpQ|hydu>-dFuCPN*{T>PYqTrK~MGr5n z_o9O()7SFtjGkOk^2l)!UQZZlv|>7%-5=o&n*G=`)WA&7vZ9dvbJAPu{0{zu1`W!q zAW}yUmJC7Ev6U(2Dw|Uf2KZ*3d5OF9c-@_O`Fn(R`9if&2LpF4L$SDNPM@8L{WTjAlYZpVHF~<=|8$yL8=mR3ax zWgs4rBA9UxysDn<_yL4?X{Dx_Es6)i9TiPsK_X_0HOl+33np>#eDR1x2o6{ARQ24> z%ZtV!;M<0y#hP=#+1QbmXL0dao9~&@mEV=So$MM5!t5wp?Glv5Q>}0gnBl1|y_{=? zra$X;hECR&YjT9x908ne?IV;1w|=ApiuX}3OSl%XXkQ5E=H8(|WtOudT9NLtVb@Ju zkvOMiL&>d|3+9X4?=Kjc?CIQwEWvWicih>2rgie>s0tf&CDP9A97`6?tzGymeP+Nf z5h=0}bIg1ECInD4^%D#sj%zwx*U6Gl2wlCM(HmV}>9aRJ^DlIw@%K|ZEQy=6{GhZ# z|1E=@XwnN8JEo`~`lQ#A{eQ*EX&DF{I}x$GaA$g?cDYww!cxn7hdu=L!IUW z$GyMSKl7n+IN;NQIMKdsl^lVjZ54Pl&1%nFSZ`aubqD$W!2?{N<#fPO>~I>2^ZL7? zL{lJ?U&-1f*Q})?N?KmK&$HEjVeBos<-JX^4Ofl$ECsirso? zlNce&u(-?%g3?e-T^jauR!jcijeo*FOj#H<5leK8$hN-~S(vx(#|mkqd1pHSmJ_5z zW)AlDQA>zrimo>0B?C|Y(<<>Uk#+{{E`6$t`w;bCdyWhLW?0g&fD&vK4nE8-=}SVk z4b&bnN;1j5>Ca#4;e@(b!{btcxzX>57PM%Iqzw=xD){4$01b9%UZ5>}UuM7!s?qFRU(If4VJ#jbuEu{fz~GheCp~hMy_0a|JiY4uT)J zU^GJ9(tSM{v8otlv#d|LkqU@q?X531s zM1)MAGECnmd*j7;MzW!xoQf~HJ+b}F7N|%dM+sE5 zgGu>rn|5>JY{~|HZFHnR&gVdNL>a%;5}E`G zBbm$8T%)yHnpbW06Dc7I;cx}nCRu{NYqP8i9E=EhXaE{5EB-(d*eGr^!Ge}KeOX_b zYiE0;fn|T*5w*F-hI%)RP83xvxLMbs6&|U0!tVA{aH;tUW#XfYvj8wG0w+lE(}W9B zeSljE*!%epp2@V%k{&9$TNv~GZMyvuJKbUQp5`1KC^7De;e-MOAMh%fhLZUi#4|Jw z7<+Hkm;5)8A}@_)+={=^nEy~Sp9@@ER0!MwGdJ!`aXfI!ViL?MhIE?@_I9R3j?~TE zf<<-1t*5w{Q5}2W1{Rq8h4iUy8`&7kevNJVb#5#ba{3iJki)zb&c% zE4i7&N@ok5-fd2tr<$PXZdg&P0jl!>Tc<0?)lhB~2a#53)o)hmh{Qd4 zkDUnQipg=C2Mo86<7?JkI^9U?rlWxo;mJ;R6dUHDKlt5On)z}ZJa#0v=eCmvj==S4 zePqe`(ODqeK%OVRCw)CSB#CZlov74*W~h{LN38R%WVV-QU29d})|Nqy@J9z-@BQMv zKPcIJ{H*IY1RhJeGT+qJLnh7xhljFmiaymJle= z%1ZHyuGauyy-EXk#1EKk(1((M>i)P%btKZRbFGRpN{)i3cFGAzGtv<5$4T$aw zStnhj-&M6T^a@zHsiTLh5D-X}&R>x4uxOKpiNmWlwev5WBYmKF9hsU^-KKC#?UAlc z$d_z4;x{vCSBHCdn^;~iojfmX>?tWSE3i&B@#|hLezDN@&YMr~?p2et+BpwU*-s7$ z2`8ZC^MqRgt8iTK@A<>BmMO$ot-(x$-OZa+=>3g9CT2ner1BtHpQnRW&h|*UsMkz3 zKj4&xM-0SXqn=99o%Xm8H<pZ4t@wRCpX;K2e&h1Em%r z1XV8Op_s*4&cq}+soRjB^GTi4k=Q2Wb>Zyi4C2TWE=op+5y*Q{aF}nS-;BV)A{ooT z)XH1)&~7Hj_CM`hQH`KCA``px1dWiYRx2C%k6@VPbN2~;mO(PeH@R7h&#@)NI4AU3 zT9nY%QYB2aj0wSt36d@FRx z310Jm&v{745_qjctiqzyYnFnMg&6^H2mGxmVK`bp{V2G>7(u^)oP&`&4>|s*Gca%7 z9Wk;c6te^iJFXcgxtRVCPa*$?j@`;E>`VGMx)Ac5Uu<0F3DI?OY=@+!b0g&}ut8u%QXkYpb>^ z!2{Cd%Ym`%bBZ6_nTjc+EC5ghF$K~0@=TT?8_uVcaTXHb$T&X-V$*23w#!$$`P1*!2I1TfsLPDD0+&?T_6708^O zsJHf$?gb~*i{Tf7;{HazVikST414WdE&_j_Bfcx-xR1H_OF)w%Sv^6p&jHhdv080<5mNXuPK?exRTBR_}uC z%4^@HEpkIt;5pUTA7?s#Sj`S<2@ryC(&IyI1J%iU*e-+9eL7P3O_ld$kIjmpE1 zDuTMRqNV@Pf`LYvk*&t$p{F&?W%3hqgn5G3FOgVT|Aeg+#U=To%y894U!pL?)mIaE z%1da*kgLP1rFNMQ*Bjfi8IhMbjf9wij9tPpOl)1-9g0hMK_xO8lg4r>R(n{tN_FYR zE)%a5<$oz2bMm;(6&?;P3&*qymT8l~=`400ofMaDG=`~&40@FPy5kV!;ARCr$PT@SDp)pbAnfKT5&N%EJ#0RX{PLqMZWY zHfP^=ANzgp+r8)9y}Ntw?!7b22khP5d(S<;J?D4M*}ERN<|N=G5GeuA$;L^*Ng!GR z4j@iDoCIPd-~i&(!%4sa#OZ*OfSLrH1yPfmJF6rTZ~)11p|7uWbq(KsZ18&+{NUFE zf2tUk58f{E_c7qr#NSs{cT{s6dTDO$0FuE{4+CLrR~_oJ@OHlERXl)-hFjNsfLezK zwR#=43II9qXpn5<5)L5xWI1vr1VkM=<_G`bdCO9`h3t|w&x5J}jRTBKT+zoJK-6W| zbmj;s@&SZw76D+qy0^}OMJB4|;|?G^dD#StsF~Hk_t$nn$5sax`Qkf($Ym4-iUk1B zAlb}|nA1G~OJ8;8a&v}c)<_&ci0fgv3q_noWVeP@sjpjAt&K#=l3uh3=R1H%R=T{+cw&H!A;u@tv^e_j`gn@0r*W{JRwx?NA}WGXB6G82`Z@n7nT= z_{D#x45V6z?=382)XM<`0MMN}?2TEYDZAaI+X#rAQ|AOg*fVJu6lpXB2m+;Z-yn3p zH3*%12ld~^_m_1*xrlt}0n)G<{kpU%D?rG`i-4K5Z!oYj(`s8}-8si=z7NY+SBKVe zPO`OcsR5#sYwYZr18+|30sJ+sr2v-FaFbF3M3+k?ti|9b=9z|x$nhXp(4tJ+wM7SS zsde~=dy5?DDjh&{xKuU*qZKj2s`PAMpA;yCcDYIgkVTd51)c{OrBZ4LjKPo3)8a-U ziN**ND(bNx90}#(|6xPIEuvfa?*qp)B3+GNPV_5ovYLQWSeL6b0J&~<_Zr^^te;}H zq!t5R8YIiukT~lO$%aG60yyDZ3T48=^+JoH+zAvieAe35m$2L^1O{G97B0jANS`CoJz1_$=NSn8{0|(WmqU1=(Pfvq8ax7^pfmZ>E4Sd8}fN)$Z zRPYU#uWM3GDmbF}iotrGcxV`;pcGoRhJz6#pMPR_&u&t)z}2!6Al#@GCILm*mHMPB zi`9ryWU4i31qm|>X#5+ARsw_*wF+ARYPp0gvrc+C7Nip;AJj_TIlRvFsFrmA;bhCh zicU5EVQW(m9Zx(BRCw7t>|u~lt+W_ehwt^8y62X40O1hx^2p-|U!Bc)tZ*-sH-bWF65-exIb)Ym8xrj9Yp;`}7 zN+OG-ILkp%+4w#bkYLRlB@))SlW|tW3V?8|2NH86lyOfjlQ_p;{_*jFZd~-Sqk`Wl z$B%!`mgukwH&}A1BoE*_^L8x#}^-< zy1IX;H?d$eUBGyNaI6PDt>Q>wyJ}QTa*Vx<0EMs?Y27m~u*g7yZtsp$FNI6`|AsVm zldOpG0HMBP9&BTCOlR!w8(Aj-5fDpeoTSB4kTfgQ3Bhv5!2ZA;%c99RA6j?vcS{Va z(J~$&RQKwqy?4ewp|UJGTry(<+&pta7J4bYN0va5fkd`Op~)CS){O=T)xARNf!pCk zz0Z(U#41mVAw$YTzkU~1Y?j5lvsMI>&tB-TFJ~(#A$HSZ+Es8 zc-pQN0>cKY(M~uR629%~zkn0pcsV#6w-**gn_K(Dxmvh^HfGpm%>*~gSb$L7t2iNx zYF^nwP>zR7zEPR3x^)Fya^`f(sYdqMw5|^}uIphe)uc8F4 zGiueYRATyB=fk4A9w4o=iJh*S-5%UJEr~3uC6kK@NkfwXqRc;Z67#r4Uix4aCQLp> zw$4T#U;dQ}?0@q&k*;af)}!3X$m+Eu*UO!U7zq$6|A-PUt-mfQj7&;6p!p4F(a%PfhG@NW_;($_zfo1W%&<0uVFLeP`uJ!HJ& zviXZ)?)=4t0teNS=y>?3T9G`7dSL*P_5#|T_n@G9$4k${11oNhssoakKKQ>@^M`oPBr2!X{m`IO|bl-Vp^JbhTFBaAn)WNeR1(o0Vs%*EoQv z?w8vSGA|wMG zg{nH9=)AblZ&H_b-Xn9lW6LcUV!Z`PYq9dM9s@sp4A$R%Qu6q6&~C{3FV}%ENlxbom+ZfBzf3Mb8-*&4I7~{XcT`J8}OgE|SD@%M!Bf z2Ax+n-9#6QRfm;?e?oYF1rr@KQMZWfn^IDjYsBuQpBE_!v(lRI?l zSnTyo2S{AVSJjHhib7ij<^Xa8AmneKCn1VaPp~}7N=yexoSUV}0hvVIk#zCI;AGO1 z0K^pjf%7&UAUMV-H%qk!j*~ESiNcUQa&Ti5p}X|fXFJS!{f;F3%JJR zL9VZKb=AEwNYtfGH3yPqg7DD6gK*=er%UIeqr=#7AAyydx9hmnsB1(Kb=62B2Ou5( z;Zs*tYqeA+Cb_6_0KxH63l?<)B#j8xz?KcL{(H19E9p>xzxvJ(;ZH7{t>aQn*N6g0 zl3WxuS(sOd7X@G>K&V7r6(CU|M%;`6Yp`^-f9YEWKi{xk=OtsWb;-PI;L}&#p#6Sv z?}(_b0|g*?5_Dq$a$Ti+jqkw%*(z`NwAwq;bE-?)erzX=5|gJNe*hj?wNy40ZO7B% zAz1#7k*s!2WgUV*NtN*ZbiaO6U-i{HwF!uMAtNow;>wgi1#jHXWphyt^G0QH)36}a zIxE>+smH(n#DBm?j?(`jpE!W1SP*g^#6S3Febv{Vrp{k?KO+G`CF-gG$w+T(Q2{$m-_m5 zo}F~;NzOMJAkPB81#y#0Ba53>COvK*Hq>H80Yo(qLIFtL5iFx!EJ~DQ7SUK2i&~SV8p(=!f{j060(|$e7uZUk&BE z+jfd7ff8y2E6)-W3jji^$x?ZO;pF39`Aj2W7zd!Y-*_Fqb!D#<`VM>m0>8xIgF4^eTmWPWg3rSDN%`P{}w;Gyr|8mY7km);9! z&0_CJNCH7rr?`#d@EjHEO?9tK;2*{Vgvvuy7E0!3;qk0pt$+QeJ7LR%KNzL2moK~? zF8%7YiIkmtvdpzg+r2V@e^>wz8V^x5^^|OG)*wMX^3qeAVD~HAVan+}!Iq;Ze6*9R z---HDz@aL0N_f{4{$V^os5}JwrQv1?Ez24tiL^W6$rNy;?OvI}Kdb-<)y-1Ps#6GK z9`05`Jy$wi_A$@!MO7Y1)aX^7pImDdoUwTjF;Hq{ud2zXxLG(BB<<&4`h`Mp?y z7!|P!D~_tHDehICTl}|UT95DJL`Oy`?WkZ#Rhb{x^3klWZ=4RBPMtK1Hz}R=QYXp{Z6xm0*hdBqi-! z#WbOR63bg8>tQuOs8zl=N!1#yGJ+&2IH)yOC|i@chgf?nIf#lC`Pr-$fYwB{q1({2 zeSIeGmg*Rjo|hCHoR8v|&OHhN5Dphf;!_J@rA6G6Dz=XElUi9E134*ZiS@AB#S%(E z&5NM*cy`8(L1Zgw<|R5@PL&{ri50tXU@8Pa8dd~99o;2+awHA= zw!6MfKtW(&0x(IyA}j+AF*ogXTA8jCf`g1iv=~-I=hL&*?V7f0i;h zoAoFZKmu2*x4Yj1X#b_jmdAx^jS=OkgCu^^zCj~^f;?mJ^m$r>6RmUSWXx>|>lTir zhbP1#>}j}Kt*!?uQ0H5NF#f=vOu>S4+dHubhNjL*1sGzUlwC?hUmKEw?H+!G;2`0N zQFlPWbCML}Q7lQg4~9XzcWp_{vY-p~=ER-=7`pgH+#Fel^fvz z2Eo#~Z!oYf*wDgYqn6jB+jQWV;i>=x1%e~X#>siuNX)~sESv=y2FdhF-SaW3C2eWD zTG9cubU=_GV8(xNF3E|!=qC&O-XhjZtW7be} zBhJ-|h9xH0GVkwutFkS@9wiRVszHAVLV7+=z3vnpKBWYgL4(RV#2x z0T8aMl~%15^{Hi2zp}bkrQjf8MdP?yc%F_o>`YR0m9cQ&c`=aez@ z%XF;@kr0e}*p$wKpdh%eR++Nw&b4ZWN+|(?BB1Vuf_*XcTLwPD4e1u4)=K84mKGqK zvUn6?8A88~cZ`NhX#s+w;D&yS%tx5K!}>CIndK-gNpBSlH_Ki`KH_*sG+at;L4+0I z6f_pC6otz>id2Y5u_C1hNXXS9?W=|TiJ^c5QHwO@+0&INt-@9N;z0%>qR6ND^ zpbLDj>w~x6m|k76_!Q!0f<3l$)@*UXar8-(;DW2a3HU2*J`97z^J?JvwI(2y1Vj!X zAuS0&F!oSa2XxE|Kmc9^z^}*@Y5&l6c<%mXvTh^Cb%lRbB32jt^)i^y{c-YGV>=1} zbwIFbwO@y=XoW7DIkPaESeF(c2!@Wa_4yni#3{=wTOI-YLv2YTTL_Mew=N-nTOG%x z6&!IB4FSh?(3AooVaq_Z3RFc8_sbF_+=$g_bG{7tC)b9+>42(DpeZasgoQ{G5UtAg z;X8Zb<%jPBbh)TaCH%_;2f9>fJ&bE1z+86TxkkR2RVaXL92lq|fA9fv(*^{|b3gbN z;4igtwlw|Zz)F}_+d{o`z&DHpN4pvLP{R$e>xVklnG!{_9w0|7MSh@YXGzik2{+e) zk*eNX?Y92{)XZDJdt< zQlX=8!~FWXJfdS(0mMXW5x3;O*;a+Yhwme6MO2QYwBTr!oDVhMt1rwFDAoXE^T3Ns z^W+v}EpO?l64?q@|2lBAk-P_M{evCLv*0Pl0|c#xH`ZT6N!Y0>aoXppf&|C)`6q%@ z4Y?T+&LxjFLC1_ZiqQbUsFVjCxT}cuO$8*F4CJ&^W>lc(ifcHh6ritHojdcK-n1rS zEI^6`4qQBy90_O9tqSLICPwO;9!QJ?$go>gf0ZN~i#n;~NGQ`QH#iy+@?h=U8Rssf z$cxj*NPuh__(i|XoFi@lbc4j|Tn9%}WqnhMa2X1a7k?M9DsIyS5?1MYam6_@v)?K6 zGTn(q;{Z}r_o`JtxrxMf`kh&It}Wn7!Ob!bAh;Hr28wM#guZd#BMIxN1IK98;%@Z1 ziyqAc$hH^P0G5%IpmDS9C=v&b_KGog=DFJSS^EWb=yc#_jj8vWT4$wWj^S%?awHBM z?WLav|G+yL2@o7uS-6N;ZVE^+35dx+*IFcy0{0MiFcu&qAaRx?SStsPY%PZs01+U; zcgJlkR8)D=hcoVC=aDn7xJKVb3o3&$@tlz_tO)w|YOkk)|*7G+$0X<5R-`ZdmyE9L|Y zYdDB(8O4r=&Z@{HWzli5c>~n9X8HDDAzZAuWzJ)(AB)t^?4cS2im;%p;~>;k9|)-Au%S`ZaMhQWj9Hv!1^y9!fW+2*?-!ZpI*qJbbz7aTxTkP>swi0d}? zP#0Pu?Ar9voxm7j(4f%}Sfc6LB~mE($aTHOhj9Gh7hNB5$bDHVUI83HGy_EUDYj4hdD%}&|3?1hOYBc(BsQ6baV8yai&4~>vr zma@i=46+oXkbHg4?|gs1Kkj|*^SaOSI?uWHp7Y$at5?jpPn|gh006f+5`j9AJ^v1N z){~j+Sebt!7;dA?jDX6)bE^Qr=V*>Fw2O7y$aBZKnu&HNtOv zfJP!U!Xhc^X<^^aoY#tk7h|z?$VZPmZdxY`Qy$#wpo%4O1`zD;uoS~?AIBoOy*q&U)F;zDUPIXia=|4hi> zjE4oDhd8EyN~2AWnkSe5f7jihgYa7G@y~3A`lKk47&05MXtvuFrfNpfe2x(=V+ODV zX-r_^(&-yY0CgzqEiX|n)liZ8C-6fep2y_2Y6V3Mx@gwsy3T_B&8K&Qc7Kg$ z027Ou8zBY}T$NB@@s7=|iS?5J@IynW2lM->ud*J)iF@Jn5jLpFKqRI`23WLN zLJA-Jcjw*>?Eg6wKfdy1{||Eu?|RBrboU&jHwZS$Oqum2UH#%R}^|NZayu z38G=wQGo=}J^DvuQ`GwF*(OTWVKKnY)WJtO#FozIcnm+$;CLxPoEI2LnT!AKVMbx< z3=3mz9y82cun*za4BxmgQCe%!kNz%03~y-cbdNG;9Fem2X7#~iA2h3uydyU=M+?}h zI!oHaKiA4N5Apr2PUrWy7I+7`6&khf$Po7syXW*+5Sl8^5Ix0a6h~-T!3sgh$13{z zS7PSkZq2=$-aFi!4Kh@1NQEU5HXB#+>t{7;Gk$H(TFP8`TkRLXi5NROwX>ElV-Jtj z-6^My(`Xm0t!RVzkmFh|ZtjuY$epXE|#l7$KT6Q$*MI6hlgd(W8ovq}^i%&#S0l zttp9e+VzACuSrms$JeygWw6Nyj*1w(#z-Xyd}%L8+6;23F+G}Fpq&Y{Y2E$|*UuI-`^+vATv@>--ESQ2vKR++Z8&>{hW_O;e^o zCP6{w-WkZtM%bq(ZG-d%DANuiGoaSFzEckFB(FqUy z@6w}RLAJLuYoJ1q;a>|Jr}=D)g%Ga(xf4{e71guedi?jW3{|G;+IMJlKKAQ9qlEj3 zBQfne67hRON~`BDuhFhQjKdueYHvQX5(n;&Kn8Sj-UZmt|OZe*jo*S4AYdMZYd53-R+C*GXMt+C-hHoj&Fvf+H(T6(z< zDvvELSNQ!{T$gF%Qs?6Ty}0MyJ>F30;^cyxJ%9W$hUKR*h zS#ujC+TOJa_UlS4kd?dam)6vf zk_Rx9)ti%0nrmwC69o^1?-_qBodhzU9pePM7Ae}H?=i34T;%wVc+JlsUhwu#!;&Ax zb3@+$ed1F;S#Cr}y^JKo@Hxr|Nz2Lz4ru_Ci-$&F*bF>$;qwgx7yhnMm_ zZj2O2@7~H%&e|+pKex-Rv8Rx5H<4mi_ZGFEbYeDPJ9HS5>I1jp=qw~BbeCKi+^}F_ z^@DVC+u+xa z`*HPcW7b}hmQ5*&LXeV2O;qHwC9bA1<Uyc z@WVRPAUlH9RU5ogV2V{6(>a=ti*N5N*Z|Ko1z+S28B{7TVy<|ArRuUjnU=*1d6(ua z5aKn?-aXw!&Z)m!KUoSy?7fnqr!hSRNsNnj5&YhzF~qwR|G$%Ta$!{xJzfF~8}cvDs)q?`rIE7)V%3vc662=a$h#N?r1OSiB+50;m(Xh zs5lhKdQuo60kE81xXLBNBV5vQ64H^(>dB#LQtj1?p@}={Zc}D8G%>M7CT~e*l-NHL zYEG0Tqba_QgI(*^K3F0sL*<047Nn_T;)h-);D5W*tXE#~hO8BeHvAHS!PFd;DQo<1 zD|;VGK}R##pVdN?RI$aCz8f|e&DEJ9FMk9i&Lg~h8IQh&CvQ|S!k!P z^!GQDE}3T(^gHhNS=nv8i1}skx1l}%Fr4$^Ns^vWu>T82Mczvk`o}+ki z1fEoOHCM-|Wf-iG`g)Q>7V34VKF%PnLgj>!>f_%Sx5_dRyO4T$aDC~+gx}`(Mka1H*N_V4V3TEhT`SG;&beLXM}PzTOdyZ%mI}~WBt3qc)8X=J!Izn76#i-e7AkGOfl)C+_>NE4 z-%$M2!+tz7V>DRYoR7hrV^_rvuD#`&}@XJh0h-U*fcg(2wkik7pEY)CHMTM4dX0RaRXHmX{AEJDBmZl#5z zga+MBT-<`q3Z-3NqN{j%jZ^&-2vMb9B4Z2c2(cN0JpvYMjQ^-h@0#tnK4wly#Pcfp6(iEG9)KN|JDJBXG2h*4h>|nrtEp#uezog+=v|ZXP2Cr$-?SBSLb|few3@qfs=gQ|9BSMsqCPv5le8dxbkG zZsT`X=yIzo>PQs51b7`R+WDd1+5aT=GoK;#aE2sfP0uQ+GVnWh5ZBFaZ}+MJC}zld z#M5CQoV+xk^~NwLm!q@ADd-IY2ib!UEf~i;<;n!}F>#xqJ5m6=LvOma0>RP5?d%BC zViS}Z4dBGlj+5RP`=6Vqu$5PB<3)w<;uO?RZm`-V<*Ux+_R#o;Ev~3mR()jjP4X#W zWBIsAe_YD|s>#A_74vWy9H`O*sx71&W}2e+#_r@)*<9BV6oIND?JewC(hIX&nnM~) zq?BIniYsxpNt?r2HR!_`({wh8HMzLd`pOr%+FCk}cCoTlI8(UL)WpdRMeS zTdP673*#PX1&~7je>;?M4g}AODP0Eq$O@6RK7$hsj9R!e=%wp~GV~=P6f(z8W`rjA9RkWduH9d(91W07`{pjkn0grv> zyP9n1&u>K56IRoh#OKU}l+dj40MZ^@oJ@^n0C1=DetbUEz9^rVfQ6nkgP`=;b7-F; zCINYQ-jVK%>I(onppxqdP^GC7U#Ch1Fp4`V%1WIr>g%RX11@seeu?=*xBWuIuJ257 z)h1rFum-RLO=(Rtvo~J>Btq@i6rW`hvrlX+L0TrR#xwp5Gi>M{tL$ERscX?pgpRhh zg@8c+m3mCHU&;V{ee&d_K661(&P--$!r#h>o}qs0D%XACP-1ALqd9oypDjjV>C-vC~DOlhrSt@@DwaWV? z@HCYpZJ~>qSsdKU6!%^<(8-G;e z0g`&CJx`Nj%K4|fUNtwQ4#fZ}1LV`M5fOfU=0cT+T@6}vH}&fAcgX8W-Zi-CDDu*f zArpiN6njPx#PqOySji`k8Wb4JQ@=bod*_+He*5Q7ih%JZ6s-w(je=q}(Q`Gc#4skN zqN0E(2J{k#^1^)fyjc#;#9goCf^(4D!Z4_od*Ro;ZQkXGTvJocCxl?JytU2$>B<)H z&@@7L>u(CUz)o0tPa!;$k__~A`Y&7K!pb;gI9H%oy46=ct_*P48Xyo)q*bo=83H(= zd$I!xf3vFk$3_Tz10)C~8DS2Lv-kdR?t`F{0Ku1m9&d>CF zZ@MAU&~cFf{i|kuoVa1EeP}=rH?27D_$IFF5&(WFs!-742#Jc>yoU(n1&D5bAFH1Z zl6yTisbPN*~xeP8eMPBJq!V5H}z2LOQ45TT1C^Xq?ymYRIO zv8^vA0~Cxj&<5&<`8EK6E!|L8%krV~PSHp@pTSgbq*4=WZYC!UgGH9ZTvwPET(|9}G_FWKb4>EP8po1r_Pl6& znCZZzEqB=6^CZ0ZWa=!+AcFJYo>kbyG3GYxNZo1{R{blE!ps#z0rmz{z5Wl# zaM+dtK-aGr3V`x1K)=orLqphjX0OiyK=d!A-rFQob=GVkkpk#WEG673q5yQOuK4hn z12LE76xuKrK+JlG&UImWfJOM3J~WifqI=aIx*))<``db@DF`SVV{Cpd0l=SM=A9jv z1mGcUrMawBKq9M7d7&zqrqFTuG60Zs^A5470)YPu;&nF;pE7Wpj+OK-?c1H~xy!3G z*Zpz5@zC;RIO2kzDS@zi| z5GTKj%|||{=s>}JBxFo!x?2jvgL#L$ER7s0cUrBjCY%P|=25!)`fec+vYG5P9LM#r zxg3Z0_gHUKKB@#TU9EoopFnEHuQP0TQuS>poLkn7^MUw}5*|EOhu*GA!)y|GYIK@h z{WJR3{H0zKISP;H=rhM;KPMUUNCw(m3)C~qdA#8s5Iczg_dLPP@GvE)MIVUaDVG2J zJ(wP2l($Fze5np$cN58YfDH%0Ya<7BD2`9*xMJ<^Y> z*87puZ0H^zq_D7%Sv@Uoo;>9>1?n`XwW%}4CmN_qV`3FBhqgU?i6ThCTUuJ8sLcs7 z9Skw8TG-P`;hOEG0Wacqko~ z``6yZrIYOd?PCKXM6iuw8JyAH@Xc}_W{&#r3~73W85-0TkG6MqKCCTpKUnK#pm7Pm z_l7s<#HiP>Wmvtowbw{_^6avJ^m7-Qnb zlMVU%S@wc+>vzpo7I!9J#3i{>3~zs(CAsEfRK$;OzNi%i1}a1{P3xnx&%HGjTtlS2 z$8R;R3{vu5pZqHPMpbNt$Ku8LSyzK;VDfN(e8Kn+CE^l3L$7!ODN$b?TEKpqH?1(js14e^-COVESt3hZWGK#L(Vd=i){CI z>mW?(%j52wurN<%*6F~#iEn~me=CnGhfGQ`u?z(Z2;vQ3Te9j06@6~^8A;pc`dHVw z=F*`^h3T49{~1LN=WeyruM_sjdR@3~Rp{O2QJqMZG%Y0X`%OvlvVw=TKJT=2+l?=( z%ZAFG$hf02fIzucil3l5`vUvyT;H5%5tcbfTO#@>-ZvEV2SQYM{DzszVhox z4H)jKL<>~5uIU+pb+Jt8ob1vmac;Be`nJiUTw(f;w^GFyDL={^p$*L3eCh-{F2m6S zwYGC{y4$J{%1;Rw=ZAzJe?$Bo9-uTrXQE&kB-@m4DI7UN*Qi-gq-*+tyPkP6VEM-a zK7`fz#)P6(SZ|px>cPzP>sN(SDfXT@l!+3NoMW6-)XN)@4MhT%dI~HCN_ZS@jaWUo=P4 zPSWMQ7SA*G>-*O4V!~kV09TsQ&q|GVQ770vEZd zARaw1EBlkeK=uA>-*uitcgjiAlzy^E2qZYyMs5TyN#&v{bwVLzxAdOTr>|MW04Xw6 z*FvX<>8!DrjCnhTn_0>E-P&%XYg%FDJi^V=w>_g&Ejn5DDEQb|#JT=^Q=*6U1M=Xc zcJKM3V(NZJbi^P$*Yvthq85gui_3TH57-lKFiE=QPbk8FNWwQ7Q=g zwab^r8!U1LhhOO=Qqrz*mcM#fiOF~WYu#TT@cWP_&+|oQobYPP(0P}IBBRA5 zGX&m{tcRXl+-;ElmID(E@+*_f=FdR2B=NrvZnWWLEeN#^$J!n9;(&hBVp+RW!yP=^L=da^WeD&O6 ziD(Bt@{2(*OZ3oZ3?yoe0z!7gZc1IYgylKrK59e~NVRL!i&Sdw-T9aBw4rG3lqW0$<+3RTil6(tb;>(xb(V>J8rhy$TY`2} zf*TFGe)$bS2K33jUj1-?cT_*v-?XsMBmPuy>cO}g2vGyTs3HAOy~ds^$Loj#H-Av$ zh>S`tAylvrG=R$S`%hOSfzElk4D@^(viaf134NLFmN z8_g_T(>0~6C%j{ktt6i~)bT3&@%)>cx66C@Qra1yJFjkVAiFcy?+9jA~N4ETi9G!~QMhHP83 za{+(c`Dmmv^hP9Up1P#O>p1{wk0{j3nYp)NWM6USGz^c1#ASoydos zE$7!Q#~_gHm}7WUD!#g7;2GIcS&VsI8UB7D-eJZ5qM@@9I(@VYR>-mpNP8{@urg1O z=$jK-m9O(ChHgPhp{XI9Tyzlhnt~X9^j>VBXcl<%DU2nxohcbLgXKT)?rUuM5b%IS z!s$uHAwGjFMuG>mT1xU^+MlAGumk6$!rNI_HC|u|IXOVq-99G;h)N?Do3L?v>2jN3 zzfmUz=VCF@msX$?-$GvL<@oeU=ODOIG?T87bD!(raXG{5z;yiD!&n_c%!?L7+fNMd zL?O@tq@#A#aCo~nwL~v5LpOe~K^bxXM(VQrDjTz2cb3cp_JjgZh zTxw=!{CTE(sF7pPeNf;1MxhP1+GU!G+*Gr^9?wQ|fdzfZ63qZ#834nXb*Y6<5+AN( zM4VQlHToARnE_-lQLqKut#qjCHJThp2oNqRALG&ww)gf%itmJ7Nu!~mX_&mIEWf7p zSQP$=Dqgi%^?r6plBi+&>LH}`r#EmvOo4ZnkQR8{Tz7UBt=dzjF{$|YVkQFt7~-L1 zFJK`orWvLK7U$q&O2%kiNc`n1q%1b;bwEZbds%e?h&QI(Y552xd7H~H$r-^kT=%&c zLIRHLtb$@_B>7ofl!Ig#P{7HPvhW;C_tJGKseK55ww626(wnca+9cKU=4IJ7#6Q%f zK=>5>)H>HZ(D*j^EVS<35_^>I?zFW5?w~|iAQQ+^Rk}r==uVd%RGERTRk;zoz1&tSJzi$_=*6S^YWmrT#?4 zzYLUEuo>8}zm5waglDW*g~QUaLa3Q&a>T?8n`0#&3dFauM>{Hrmu0~aA%c|RDa?w- zJB!WEcg2Q*xxSQcsNTbC3>ovk zKwI6FZbF001u}{T?3-gniX_}9x3joyaOa07;VhC6xhL{n#=@^|MKoax67&mJneb4d zBLR#>JKARSGMYM`zj`u8-Oyei-tpAoV~J}h@Ub$f(x!tKMVVGE&2>ky0K{4KG!4A@ea|X;qjhqXFc%m%-LT+ zcXLF|v#3}$v0{ZHvASD|K5>UKZ>E(Td~)q~3$V*&4Q_-mqk^lXQt0~;^?khX#8IKa zbiv!hJ-NpGnI3nr?iSH$K_i-RxOqc=@u!2fcE`SOXnm1yUEYei_=~(-<-T6~0#i>y zsw%~}lKhii^&q~z&w2k%I+`7;QVShf)q3@|pu$?WJTFNt7iCLC=S&xR(R6!ZI4{4b zyJ@x1@zBXMh}gBIzmwB+{f*;TBUqbJ@?$eNt;hBHozecB??qlMv~WI=$jHd|H=_Xd zKUn$ub3^!zz?1V*QzZYF%kJ@eczT s=r(g3^0S5s)HPI!FnKH0jb?BE5GA zy_e8K3nd}R%k%uc&pGcubN1}n-LpG0dq4Nyx%2&6;Za4n9mc||A!uG>s;{MCOeKp_S=_u5YL^7J6Un2P3 z)*@5w2kTLp_TK39L@6YvHQ2g6j4?J=hW`w3mfkjbc`l0e4pzL-fmV&510#v5q?2UaKhaZB+#zflsd70rgcY4WINn4)TV@%d zsE6({0k!sOS6Of@;?BU!9L1O>T3{=uA_Y)`gg&r20G{$AhdYw_I}Gkq1249KoBwG{ z4>e!EuR*{=@%6++#Us&{p+K0}GH4ja{`e5~vyS8NqvFvqp`t(nH&M)ZsP5~3;t(-kRTfeoe0 z_9+qe=f)ca?_6PV)x3z18N|Liv;r3A=^{itiMiAujVT_LNnWPvkTgi*aAGbI)N?`G z-zQZrf;b!_{e_L?lPJzpiD&>}jF4sk?xQDpkjCkW9;DM-GmEFnit`<{eAJ!c&UpiG z@H|8o!Ypjl3yh{GYLQBBEfQj2c|+&iyRC4Ny5d*|EQ|?8TYo1 z8w>DquyYX3TX)v`atXP)STS){IgY#PlmIvej?0GcEUNX_LUAD6L-4jbtOnRtOJcov z1inqsYohO{9#V^kZ1Gi;KNzh@ zS{Fg=2*7e#Mu{Ng9Fhaff_%T?KTN$i;5>O&eC`C*M5-TfoS_ZC^hka@mQ;81@)bQ6THB_0f6otG7o?%+-I!QwmmVCtdH+9NP@ zhn}cIFd>bQ>XuE2%cNoC1rLFqkVYIKmBCiSozFu_5K<@U6p2|pAQ3Qk7Q>hpPZlq| z&A_XQ=Y_<#OB}Ez0elGZYzQx9)*?lmBo(tDmNCRJ5#sLR#dq`bG1yL*|8hL+xU1nD zhdj(7#KRyVflj07V}5q7SI`~G1*v65#K zh-WzzfM$`tBc%_zNMk&kD=`EC81m{<&oO6ZIOyRS&H%EwxCcAI69-kn6KBLd=q=!3 zGb|t0b4HjXL?AF2tSAn;c1D^fcC6vzF!+Y z2`BB5d`X^*#AF;ga0rJY6-NwRCLyO3kJCtF#8em^cnmfQ+mj-8ku4q&2$Rgj-LtbH za=65DC8_`HtQADq!tB6>0T&QLC4%UW2SJV$w~?pui?oO$*oHvMBn(9AFC&9(%i%zf zJ@1pDn{a&f;xvrwys7MBkmw6LlEa}%w+_0H%`js45aEyW&iAs*Bw{`AaDZ@(xJ<*B z;r}X5Dxx)!g|NY7h&8ELlu)@ebTUNLJ?Mb%!tttC&6zp^LBc6sVhK`&#jC?K*yaw- znLtncKq?^-Cr=h}lq40BBPm&N0ftAOO%S{<1Il)h$BTq;Vk0p_bO(3nd7)@{shS*A^03P`N z!|Wqhy)Uo28s|)MMCOueU^`N0i#QXgHA$GX;k=2wkb-cZUlP-xIppoAeSd$ z=A_FZ{2cKDx$llcLk2@4jRmZ98hL`>?>^g@|jX;?2Dgj&s&n ziSw=pus*Tmg=ldg#tK~mxRyB6GL?`Ko402D-l*-JVrbf>X06;K&%_5}QV-WC?cQwe zJw*a0t$GIE#n zZSi;1uLVVHT=Ig6MoA*1Wfgm2WBar4vkt-Dk(sd1B{pDt$2xtn$e78Ad*Vwv7}iFw zFa3Rap^DTD-4+XL7g2-dILpZ&27}UF14JVj7tVC39$ZN>oRzI{$ja=k+wDIjEAB&4 z?8OLxJGCpyVU`LNFK;3%FT^&@N}qKh9zy0*uqB-%W!~j=Mv7g{)JV^#5yz6-6PaR@ zFLv&UyQdABj^B~eHvb(hnoC}nUk1If&-$F%`MXusEDPYH54qVOsL#B?>2GFY8a`q0 zEYD-TXr{*3rd>%&0o?e@&~CWF|rqI;?9@5 zdXqy3-IXfaiD}*)t#V}Uer1uqJLfnY4IEbzDTUn@--uf@8iyNB9y2%2bPm^7JbS!9-UwiSZC%wu>_#{}G4nOy$eAbL7jouv6DMz~BZ}nJqJ@rQX zB#f>d#-tsxp`ew{5##<&8{1kx1={}z%CaSWobjbJe@G<6IPOV50wzp8ak`rNWD@?h zz4ip~WAMez8eP!KdV-3|3lGNCXG-1b<}NqIgc#oQn6e2R(!GV2SeDrjo5d_reKKd4D&{MWa_DqVh}DK|GuX|4^R7Q) z_r%o_XF5?v^V5yn1Rm+xr$&|ko@&tg!;ts0@w?iK!L?S4j_|Nv!D{rnO2>Ii^3rN| zOiLy_ID?H9(9zNTJ(G-~GaQ!gCHq{++5 zv*ltQltE%kikaYit;5#X*OT0OoGyCQkqY+*ev-zSY>&1}Va#3Nd+Snab~Ikh)cF`> zcwA2Vl;#nuNxDM+ixykZDgf}wX?Du+ycp{^MwW(?5j3#isH+y<7k=%<2-%)&{CHXn zUmd(KV|4n|QEVTPnua}j-<9r;Dk8-}uO$Kc>IaGq`mZ34m<7w#G_vF1U!$H;nw<>x z>nz<{FXQDi8E$8*E2Dqpc9&x(7MlQd@H+B{2GBnLS>oKu<=LAvqzRq;2{=t4k2X#H zDv7%p`55lz+&3Ryt=sttN#=Iydb_jcjzUh9tXt7e$?e5#iGAZfo8)xg%+q*Z2{k0# zOZiUAiblp+=-=b(n_0}e2te&j&7Rv_ZO+?v+8Q2V-_1Dt>(prKq3OY5 zAIVH>RC}Fe!Yp5>DkE_3&-lwf9ksmj_k0g<|7?&serp2QAoPm#-u4={!Fbr8hU<-( z7=ud%x9lm^C0D!GC+L3AUpI$Aj)Y=l*Xf*nS8xvaA*~+#kO*Ti#;UW<^>wq5dFQV6 z-2U0A-MwR|tk((h)UsQkc$D+tO96kgV6s!#CA-?1oJ^PI)8e(n_sWq5@>lhB=AV!x zM6|6rxb_!{UNJY;d7YV_^X7s3zf0`9pVteaH?K?Oysg=5%WCXHl3gXlNDeLT~-|p_00YTaiGmX(k{^!2VErry+*-*Nxs=WAc@VqFc)0Xoduh;Jg zO35Ewk5%boR?n*!B#U2tKqA~#U+(%A$gfj-gAauNDJC?nu6|i&GiHtZ+lB`}J>W7= z`B!gV=gbx~&W!tpli9qxejKEcvELdalS}0`nn7)w<|qTH*M1XsNMp63+lBs|E)!W< zVhX*668~0=a5R8=#_rvp5NGjQf0K5q!3%fwvg15(CbW%gry6a0n;o5CyB~t2-29D-o47zKk!`!Fit^e}^zC#DR@0{~W)+y7u zcS2u$bC5Dom}H|h&B9NC@E<{6zZc(9h}Gj(ek6Y6w3ZQewo;DZI9OvWzU8zj3!3>{ zwbz*5hC6I7xml!Wb-83EE^1nvP}oL6w*S&-cpu7HO0{;YH6)8?BwX1R&#EX&xc6|N z3zC(QZ}xl5N<@Vzxgq%$e_l&E*~#(6w)oPA5OE;~=$mbS*wRMvpH!^-+^)ArEA1cM z{)U>AdL!6l(ry`4Gf7A0e@sN^-2bWDs_}#8GV9?pU#%8A1_=iHGVevQ6H^G{$OeOK zG}YPha^LceXri=Cv1a;@_-q}fL=1Do$Aufm!(fL_qX*k==xpySaS_IDA6j*RF5xUA zX{UAW4I!uK>0{jGwI*sPgf)|x3%FKoc1?pYSC zL(}c$;|0_oh-9NC|(F7#ZlC$A7Rpkq81x8ZVweo7U>7Dn*|O>>JtO#kM6oB1T%rLm z&&q=48&B`<{B&?E9zbcYoA%)gVjeEX9Ny-BT@}_@1Z$LHMAr3!eQ8e~rWD19u75tp!%=|rdk{%CdRWfM1O@ZI0v zS4uGuJZ-3{&=A|{r1rhyrq&eehm~?0bskcMo#D{hZ@R3D$q(<`Ez{XFxQ(@cmiNx} z6!^IG)3Z=qRS@kt3gvfTgpPE1{`tpY(T6gxcfUDTGkwu-So4{-l_2!K?`0BN7r)L# zq{o*;p|h&-ag?D)evwf!afj914*y~f<~_rOe`eQ~=jDIhUqR)}3}RD@&d1Xxufr+P zS|0{F2lrS=`V8l_!QOMn^yY|2dUGg)Xsoxl=<2!~izudw@=45SlM;dRlu)K)j(8t4OXf?r?clrI2u3%p6+_5ZfU zE%3^DqWq5BhlLxnfYn>#OTW=Gsc!!+cEdjz;G3f31*WImyCn@oYXn}pQrr3Mdj#%thY#{7h3FL>(tV{Xmp!Q>-h&@ zfV6vF@~s|k&LgevoWlNBRY%Oj?l4lHaurA?Y3*Y&Sy#$ zeK!?A3^|b2NpyXE9Bw&}+`hYRG`3MoxAjI7R{T9m;2HL#F8kS&JB4?h8gX6XM&H+# z$f(i7x1PbL>Vi)pO6xmSW}YbLQ|gb;^+lxr>evKk^#Y$?bOSaewbC7_2EflUjjV4j zy%vshvn>2aXOB?0x480d(a>PE zD}GWc{!jyV_lcEf2a3ga)coN}348+P>EM`~EZ>!gd7P>OCANO!FbZ)Cxjxey!~bPh zW0_OS&7agAaa4%pJ~uh&wQ#R?wwCDF^W4)ckbD>=L`VnE5-(>dB%Sfbr%fF4%i9JG z*XL_v`%L_7iKi7datrp)MDQl|{ZoQ46xnCw4GEo#PIuYPmd2P|)o=|@zT|%I$XhdP zau0Z7+rnyL9#1MJ2Xk}I5=^Bh7-zRmn;nyS0ZRLhIn~T1v{^2tILp@q#gYGK87dL-dyHp@*dmi6aS4T3-TL%hN3keOIdkxVjWF+ zE4bA&!3qawDlUfcFXy!#@w&QHO@~z4zB`)VGl(?1hBpqsjWq>p#BYgd{L)1lyi7Kc zm+kk0N&~38Rh=i=hR7Dq=_^~mvg${_6nv3&Pv9UglK~X@U)t;o{@S6?)fHD@q#y1c z?*gOaA9OuDjjsMsRCka5-t5(1EyB;v=0x#!5nHc)MariT_!@XDeI7TEpgs5#`i`ys zxw6!!K*e($Ez36M273GPpkFlZ16YKrS#B+z30-iz10rt&T1C~;vd`9!Yc~9S*~w&} zOXLdp=wEzc801}kKC0P^!A_$@lh$$P3yF)+4rK}bNY{GC0ep`!i2sQcz8ZcTFWs}O zxSvyawfx98X+&t&>E#TCk*5qce0g>E~+3rCqO4JP8#-j zey}&q%yu0Te2owLMYX&JO2AwCMTi3@!^i-`^Jk_$k}SU~A{qczyDjI@3(HhP_}?O# zU(h=lO~k#5i}cUAzSM3B?Sya@pJCgu)r@BbH4_3f7LX^mw?cQKDSIrQqvw#O`OO!9 zomX^!k}D{(kC@SR{)&##j%fcOg}9aoS?l`c%X`9$8WTH-bVUTqcnrF3Z~TBg>8~Do zZ~BH%=>6h{>>qToJ#v=5wOTqt!wycCm`95IhIfC|wy|cKYtLnB{DtPs-F{pyz>+W`4%DP`ckj z@Os`3XR%HFL4bgw^&<4N5J?Z+VL<(2nm@cH8FoYg5f5K{;Al(I^E_^7`S=KRNhB59 zE<`!w3cVN64;Si=5>FPw|0d3qg`0LXJvqW2hL0k?w3To-`6m8;U;J#PdkVPb|9L&8 zv~In-;j`U>0mZ;kmDv;1{?P79A2cdPZatD_WrcE?k-D~#jD%niz(nt>-#u$|HZ;76 z+X^`3xuAoQmmGt;b@=l#P1kfFgeg9(>{#sRte)xii)_oV%a>LIA;U7S;NC*4q3;x1 zHy-@LI3c@V8dnPL#)SPy!>-kJJKlf0VYY6!SlHY(%n#eQR)~BXs{NyekdL^T9g|`S z(sG;nUF}>o>7|c|dxZGCa~DUAISm^Aa$8HgkVmK|Lt5@cvnk%1>}440V-@OGwU=lo zrP`c3`01@j;#~@Tqhl3~(r_zv82JFoxD<{W>=U4^S182#PfEl3^TpOlwfsdVpqE|m z)e6+_^VNB5)bdYI`!}BFM0P)NogJC9ogdXFq}T1{Tgt{xmw#eE#wIOB(B{L}$jGkfQCQ(~PkQJ=}XSWhZ)qzf;W_RH`LX_K7V<6tU(A^|OdK za7_H=Hu%>^3N8-BZ3w_QgWaPqXV4aD_63rspVKAZ$i(=rtAZ*sMvaTiCqh2ILmVC9(cwj6&p)@{3SaV0YvQezz|UC=kK7N2CF z=FjJB_8(R?)tT*5d(qEofHaPrC_A~Q;PH!4@cd3HU@cv5-!|>+23a|$3jO2*v*4x1 z7ryTK%}=I*sNq9;@|v>>{t3MqyWrCUar2Gt50E`M&pH6_@cP}(TqPrMq?_^y({b-> zg;hDvo13pfRB*%c6I}LNy@3)%Ae!(yhckn{2BYnTWx07Gz7&`t%H>2p#aSbNVGSLpG>bupZ8jp-v zV2?a#Jb*X-&m$14Ghe(%=WgpwB{9XC6%_wqDY~E<;buJMbc5YYw{cxOmSlxdo3n{Y zTDi;F3;Uzc^tU{%6DXcnIPcy9wM6Ya+$BH^y5`CVyRJox#%J|aXH6GuRCt{y*2i{{ zk3|S~%P_oCT@q77W9TgV*p9W!9?<-^^ZF@H8QLXYI|3E3ux#p`&Bw14DftT)fg@T= zFI4)lx^9_6MzzI%L6GH~&tD}%`Q7~#CqdTlfdO(uy_LyZYk$#K77l1yBbZGFFN-eT#i!WXq}sR3W)o+_~_Z*6RXrv&$A4IvuY+DaS)-t1qtgTT{(ARzYd z#;dwBzdDHky60pQdcGye?3z80Bh5>|{*@wZxXUO|>jca@UH|S;UTc}`4sUl7*qxt= z$3L3j6#0|z>2=0m-7i7@Xm%}Dk;rt#@pNUW6nW|t9Obr6c2f-GWYtl2nz;eTJzj%i za(*p~sPibEH;B|_&D2Zc>9HSv15xYdHxSz(m(!_aimSV@!sRbjmCp7S#P%P#raBzf zb!)y~pseh#V`@Za)UIuG`YVv3G%xh^D%OXkgTAz}8$p}khwOx?|@4wjo0on%8pr*Pb)@}b%&|c zFjxE4KRNl5Jj<*L^E+@WL~SzI^Ly(hIxZY=aa(aj`@>y zl*}KRdMg~;?W!iLMIojPnfYQ$Iluj0Y1?^Awv_(5&h|pmhs!SUgJ}1yy}0eFPrJny z?xPEWwrDDrmx$0%j^Qu)Oh>OixR4yV7Xqtn2FMRS$IeuZ92NS56>$yf(R?%ZvTPTWWI6>M0oc?t|ZB7mF~OUYXh+@esPW90B_$8QE=1 zoEq9TB_-vI<~p;iR&v?G(Z@^lGbE<}c>%4twNUkFxt}z0Zm)Q-(4nC@3JaoAkMeMU zAHh0P!k+T>-F&biv)wHvKHauFqW%x%+H^8qXg4Fo<=G3TZlg)SUd**^nlHr-k+EMG zPwwoDbdFnW=@gW`ECwmpRL>`g;h+j%q(H$gzvSL&o-~=)zxc&LOgdVdF*!1=8S|ic zU-u9Rs%V`((|@9@aqpa~=B_Pn9$zz7CKJmZ@m7g=&B>>m z^tPvf=nVoG?ym%`+cd4cV({(<@n(9QxX7blks@oWnWJP)IDd6S_=iF7ALv(69pO#q zlz(`sGIV~hiqbPX|6?Dj)6O{V&ETft8g}|CATGe%?r?bIigFFE-q#6K6O#(SPuamZ z1-~zeOnaycGlnX2xR`Etx6#zyjNB>C*oP&9LjOfK2D5--m|Yp4GW2fUnrt+*6WTZb zd)tWrlIgG%^Ie+|VHyDXWV9?R4jj0bz1ED9a}Epsaqpu|Uv)RZA@raCT$zynr6Gs! zO9iIY6fW~lVAjtQe{SO|AkHH*jCFQRw(K={|U7Wq&86S)NUeW3fp z&Miu_OQ|%Ua!T*(+&!7TGajB0vt_4HhiPeMlg8w!xzcguY4Lvt%3PbE#?h3MG zNc_8`+|CIIiD0{?a!3p3P{G81ttNHmpJ2gc0EOWQ~B>6G`_iz5_Iu^$8 zA_m1D5PxVf3!`7Mdf-H*oxR)V4xbJkAiW;}&XXJ_=d8gsIc%+UF`qqy;n0z;cwSm*h-BE9+n>d$8Ru{*V@ z*3EQ;mojrX$)B3+T|=4rirsZE|5^zj9}NRr=Y?B1$1+_-K*P7BRaS2`$E-M_n>QhA zhnWebI%aiFRti9@|-+*M|3eyxAP^2)!)UaLIiW!L@S24_4BtO+K={G?;fqWQF{Hlxjo zQ64b_%0K=MNwqtrKqCSV?3fujOsp@5j{X!6jNTl3t zck!)J^3QIl$KT`p14mhvG@rJMyy&g_ID?4Yl{ap(h;d;!3`CJpdP1ex36fJNV{*F^ z-spc}UeYRIEZyQ+kq#ymDjjlw8GbwnwE|$DvpK2T=#x9|-lv^ijSU9U${`u7o~}`C z$52N9*?v)6X4_-=<*+wr%-n*wVjEd}R)%^|MDCTw{|dG@3N6iPFc_rzvh~t8T-x2E zPTYdqbq*SLgfi-xO=5|gyKi)Bd=JZ1f6kpDsyA2jP2|PA(=DQZzH+lVAizfj0Q@Pw z>*%C^rUS5aq2m)4xhM8X2k!jHv}mD5lK+uD;#0Mt+zcn${&FH#W~(;XaS}YmScK@Q zbMpWf!$XQ~pB<+P_Dl@jUs{34yFK;IZg4-621SXPc3#Jj!`i8R*yF8Dcxr1-ztwr4 z`bT~%Har(}->G<+x!uC7x^~kpKjz+4VW91xHlAje2|Qbv^L(GUt*Tp6}k8WJY_)xDH~@Re$KEk5P8v1_4|5WYD)kWt7n`t(Asv zegF4!-8W-N8dl{tGEB5BZ=~l1rN=d9ejg7GaqDd59Q4{waM;~@*20+Ks?Haf%f+1$ z@seiHB`R)PDr+pp(1(Rt7WZ!xITm`~V>>&C-UVcBmwy;^ystCmMlbSu=9T<<7C_HX zy{E%pdyp<n^zMXHaga<8E3 zl0i+njN5%Vi<>emAtO?pn`Hr6Ub(L`&PNtB^jRsNzIl|Nr%sgZ7*$@NM}$;*?2*;z zYX5d3a#v+@yUD&f6?pu7M>CpL>HSA51@=9wpbHgi-sKA4@sS=T0jlVc2zy-^Xr5fG zkblBjVdjZwgjz(>ev)wxNzIYA^lR_**}r+w7DGt)g@*_TZv@K>pvTb28xZBmFv~RO zzZR0K(MKK4^fS|VNx3t7u`|!~9dYY(u?l6v%=SjA)9=G(iVHFC z0qfkI%u+k(y2%Yty#7Vv#=UL-{>x%u#4Nrl#&McOEHPQKCHdUizoy)QNwni0a1;Sz zLLSu8WzhF3gp2wvEVjw(sDe2CQ}ncbX*-*S?5>>D6ua%n&wO_f46l zI-^OWiomWKj)evHHq4_vroM*v;zXwQ zWt06u=YK4c!L5>4Y@%*0o~1MS|0o+7wf|_N3g~V_`o-&ny8^_yr)P6NPCxJ4&Lc{* zt%TG}%ml|lbMr>|SUPg|FLPZi#X{TzVo`(G6{W7PwXb3P(M#QsDMx;K~ zC(w!De%vc8+M93wct5?Wc^K5B%Y=@}y`&9uckCyKESDV8hl%D?(l>nFOb}tpkp7*Y z#`y3VEF>|ycVN7m3?dH@{Xc~IFTW!IWG%<;bjJ$w_+x~wcXY$dF@rvo`vte;4QY@Q z3eA}sdRn6=I?VY+zH*}GH(*sV?ewA|kK7#^$C~N=)Ou9byx+PdXP{2eEkp6z*5&nq zR@1~G`d)rDI{RmU#p)AHi`N%@V9-e+ z)Ad0_8DxfsgmMJicQ29zXnwgFgwO2Ok)QMwITOl_Vp(vu^^?`ASMI2zYb88t4M9*o zoipe>qQq4j?Ga~LyHlesPfjPE%lNC)?cfj$92m>Mh&NtnoVNsdbjJ@Is}6e z+a}G@xj)k0r|b43CbJE+bA22JeQ9DGk0n<*`yS3!aA%rR?}R6hzR5H-!7^4pQ_U6X z6A?V~TawntViZ5>9rU#zAaYW|@vKSAEf|~>Ca$w?ASD8S15vt@ig~o5 zL1M<32E<0Zyu6qHf-dQib+oM+e9C?1#S*9E6E%cKmjyJ?Z7u`J$SDznkCWfwUy`ea zZnw)qW29vNkQAn>89)7)*cpO2rXBzfp-_p6!QHKY@GiX z`dS8g-!^rEqiBictkHex2%3hzODq-loGD<56IxaK99<%MWN+|iH1W)$_`c7nWynQ> z5_}Z6n5whS?`nC6bFQP}8QF#U80gc#w|jGupCmU&`-G8>@8*_vvlR86P(!ZkQg8wl z{(ZR7B(r$!)_7UjebL^`_5Nfh$204x-xV$WGrC{Mx;o`!d>sG&t5}T4RR&TZ_NJsLo5xO2k3O?xaR};)HZc>)j(KPh_1oS;FU+&FA zg-diE$yHM?-f9);6+!9(1FL3~aZFm@1YWG0FeqB^M^WJWi@Fb57uiQzndutYiZ*_5s8}+OBYjI| z*_DkHBToORt5*W_7NQ-nulBZ%SNH!qxl4o} zZ@Om$ETklKPHPun#EvVSOpAhdeIDowDqic!a~huyggn5j{f-buOa4Xl7BN-LXgl)0 zf9`oa3RJ(1HWSnkm2oeO&#K%~G2KNf8uDbMEvd<;la|~2e|s+_9=dFV@^ca-p7t+3 z9G~LZUsw9ZmA^Ae!+M|QNufwpFm7zw@LQ_JALN_<9NX*8XNsPoH^JUi_J7X7bb?w4 zX`7mOO3z0zmi{W4+l9p})+$Hxp+b;i(8I|01JX>6q>|hmy6_Ac9^>8jMLba%;4Df^ z^o^UK;Gd$>PZYPMquAJGLUb#dd~e1YkOg;|_YOQJ^*8$yGGGifS9as<;eP&sA4Q27 z&ny--v>>HG2Dl8~E~;Kt5ZceVZ=X)=ab3=Kt@cX4TAACSuZI0WR$Idpi#iWkjaEPC zgV%lkoFs~D$lD01Ug$d8(5BA4JC>czMQ2Jiukyh;>&Gem@e=MM^0_j>r))3RKNt5R zNGm$v4KlY@$+Ei}U=zENb?1p?moQovLsu&B_a*k-yXQ;R;(q}iv;Xq~cHml4{S~fT z4r==4)FUj!y`;4JubS)=TxQOw>Z%M79)#S+(%_o^TbKv#@BbpI@@rT&>9Wcx!-vr@ z^$*SOTy2EJqXC24q6C)o6GOLjn&f&`+!FJyHC{V|x_saV?$6NAyL(1bp+5}%J1zbj zD%`^Y5bx@9Gy~Jpb%{>CY*t(S$L2`?o~h)b1LR%91LTh3`2>(K!7vz5u9lmE{URiK zGO#%|OI`o!RAQj;VMSt@qT0EPu5(JX^nKsSI1< zY)K0Y5)*$?(`o9}F?`XJ3e$Ycqh&><(R-%T6NJgX*C7~{QQ_Jj%wt$7sz0bm&WI~k z?^%f+Tuk7IH=NYlsMO7Sa-2Pz2iPZcLEIghI5>+Md?UA_LjeQGLzmoqExd@@?vL#Z zFoZy&`H2w+9P*hr`7pwLT*FxQ7($h)2UYB&WK3EC_1`G9I8$24p^m<;uN$M}2TWCZ z9`a@V8oBRkw@CqZj7cK;tX+i-ogKT|EEh>jTsx7WqXwC80pm-OB=;gTo6moN(x?9# z($_%~RUrwr)|a60(h%w7eeN}eWpQDS`RUp_4Da1({s4q_7dmGt%J}PrkJs;7pWJgn zf7W_pI=HL&T~NDfxT{`iJf0g?{d6<-J< zDo%sw<@X?IfBn}5fUXbv5e~@fupRG&b zjqavy_}sPeUKf^$B-S+E2VncIrsWoup8qUdsJT-~+BeJ`7VXNmmp?j^ z=ei!~4DPEjecos5NKS`rA4S_QuVQ#TBrE-=)SR+Op~HpF^t0WGAK1l*8qxa|d2`D-SGh9j-!oZUZPEQXO~A9x<9#^R{x+5|Um`$S7@}nMsY&Be#KA|dG1sa$ zFWXQKt~9KV5d2uRYSbx#zOhl~8tSx9xOSK;I;XY!b^Ms_#!QjY=kHdD&?KS=IX7O}6^ z-W>B%C+)F~Q=UFoMdtDA&j}p2q$Sl8%>+FJ#V3C@y8X4cw!{vX0lKE%Pgg>t<B*gm8RlEUwPkx-p3&Yx&ZefB>#CPUV%j|cH^BtQDBUVOyAM5P= zc0(-)s?X8R5i@u9>SHPqSl_JnP0-0oLLZl>vslhTmhG}!2jaBBH+V16-L#{Z!Z*>X zz4XF&`jh#mj2Q~|UV9-&XLH9`$>?To0Onv-IVLSXdRT3pYq|^GY?-1wl?)a0NY9+_ zW9%H1_S3FL8M~A|cyqMsoN`3w$guM7IMH^4}?>tK9y|5q7_lw?^cX6E5y{%WRekw)zRyJ6eoq6Ela8iu6{8=T z{J&~Z_xa`#c0oJsk++8GuGLCwtmEX=_?c^w=$4VriiBBr8V}@mWr$H|uO1+A%Pl&s z3xZOthAoc&H+Yb^e$&>X&P}=6>BCLfrO(!-utnEoqJx+2A2KN$1xR}%ue!JL#0U$3 z6+0cITlY$D@HNu6MVQzt@g<+u+ei0h~~Ao!3A z5+vkri5O2*f~dJ_h~dX0{-3bv*!7PzcTD&-SFca6KdLZncdl0R2{gn;%1f3 zOxcjh;k|q&E+mihM!A{W#>h1s_E!I1KRzT;{!p+&L38M8fjW)VT%w^_Oxf)%&*+#> z?t!w7;9BtBddHIQa)DO2w4-TSX)`79moZ0d3GJJJI$0#!bz?qhV|!CMR!|>otABWL zty%&fRT6C&gEDq2$>_Zr3eUjIj{l1FW12ZxvWq{?wKY(z7c}d;fwOOO@G8T-M96rZ zej)Hff7h#;8oJDuK@LXG>jQ|-4m4MGwRqH66NnOR?J04~ZQQNDvR00exw_V|dlh^S)@&jkiESR3jZA_}%;4S9=9dT< zxOP+XpCk0%kBV;Dp=n}G@mq0+=v1tSTWbAiB@7Hb5abTRJqBy-T3Z=chFgEKpNV`b z@Z^`o-c&TI>QF%Md6bmf=K*4Lp^4l~Np*2qHSd-80e4ZHk?rbL(4S=R2Tix()k?iA zSK_uQCbvTPfJMb`J_bI$k*c#N<7~XrvA007!2NQLmSU|}_9ix(*^e$N}x>%saW+U1!r!AmZF!Uvt!%Z%YoRC0{ ze@o8GDwWqf_1azNt+8=`>(-jFZ1iCK%Fxl!Tr)hJv6m0$ysABP{5BA-gn<77StF`(BH+I)E{K>m1eIisA@F-Go zkZ<8&nKS$|xqXbj$TMLEMk7m?*OB@h6!?TncY5knO8=8PEhXUxspPpr^jdBvEy52= zAKG$-+%Z{;+GL~^@_c9~Mu*p)=bfdm^14xaZ%N22<9dqgow#u5z1b&wItspl;}JUb zjh{HvFI3k|J^oN^7h{9s+6DcWBBU*9SwpdOjx_UM_bGG*t*RaQtrU*xpI!eHx;nt1 z<&g4uv%q)z1!F0@ zl4`p>uess%H}R;_Gsqoqr}Qw9o!zmNmn8S}>9C05@2(ClW$nGzKoYkd8h$W;yP?iU z616EnC%PhDnpJ)J8$UUddm8J@Pk+0DRZ`pa#`(#j&u95x;;*gM0@&rf)n9fGeLb4r z!au4w;A8Uk)?}&@Z6PITt04Yb4a)7D?VmZ({)Y?pg$b#*d51BJKC*)^xrDKV^4E|S zXy#gy$*^lnvGpNFF@$}QBw7car6exS#<_?>XKXy;u>MpCsf)j3rSRXNzDBN{qsuXC zR#;SoG^a7slcbM#TUkh4-rDJnI;b}mFGTz~>w3t9=?LZ76`SnB?Yz>4dRS{fC-VO7 zj0H1Ab#g~l17jCM!DRU;gRQG=FUPIZ?1`o=r}Dbr6=VcP`h}z4G-9bbS%kWuTAwq% z$i5ia7qbeq9_7eGvtB4^<-g_CS2&M#7D};oJXpVJ^;Q7Zvz7?5p$AHFK4Bq<;~c)LBUn8P{a_eyL7) zoVk6cIBCp0`hc8ew@IcZ1R7zQ`o3{J?)a`9pB178_uB62;^jjF(^xFSIyu8w`n#x4 z0)iP!&P4nU3Me#%+5K9)-=Lk}1bI>GHap`q`CgIk^M&kY$k+f9GoZ{4YTQiZ%)@ux zHi`g{&PojPMN@L7>)hQ*3GweWDqA?dZpui-{Mpb=`vHH5B_jAt^S_%G=H3IX>aFKQ z6~ib_?y)i+{1Qq|rXfXpYgxYHp;{QDHd)(Ln;NhwmFcdb%kt zd#lVcdVVM4;?~APAFs3{a;Bt*USC)k<cOmEGkb3|!;^ehl1lf>tK(=)H(;B(5^BYx5g=$F)y7Yhj=>5*U zj;5VQg6=5!S~vx{zSysz`aLYbvqn#6Jazl6PDGZZ2&1j#XmMK}d2l)A8x-flB}3GE z+V1to=`b(>b;j1+o?Ve2buQ7(!g%=oo@n>k4Fbi{urbwWX!-cVKO^cVK6f}m0&O2a@(L`tM% zASECmp)>=AgkyBWKtLEummn!p2Ho9^X5{FO(LG=r@$UD$&-35@+jX7uJ@>iqPaT9P zj98QR=X}-Few>&`HHjDDq#H{Qx>z6s?F645%dJL)RFtnMGtclmDq2o+Havkf-aQv} zyerbpxXd{6^pVSk&=mtN>fQTp@H!Lk(*&t)cx;e3=(!)aA*nxsW4-K>knJ;hI0v2W zDuQDcW;~(j8R^@N_Tuj>wrckY&UpzR+Wnf^Zkazfe$$R@pFWp7-};svvpnhv^HNcp zoAr6A!bs7e%yIGPB{jraqg?L|(U{h-NPshoc52UfT;nnrl~?KXdgn*~KcRCHfAZ*4 z8x?0((MZgI(5a3~BscYYPjC8!Q8Kk^!(ElTI2wCWsPsFzNbeR}f@A}#jB{J7TLlBgrv%hQ?O8bXA z-bmsNNMP17W!`FKoV&wyG-+A@m}0>{hRP1oUY|9(?cKF7f~=U^$6pb!w_YuFVPm$q z)4rMTcT;iR=QmZ&Q2GP$L@$87dhxAN|wm+Ofl)iG2|cfb!NlQ*PmkCF}9Cx44_ znPSKCseU{Bdt9`2A22Av>RMp;;>&k_Cd;S`MDEW%qGZ#KMU>p(+H_<4x>Im{ki1-7 zVbuwoU7mF|2K72R^laMQD^Bid1(bSv$e)djg@q)v!-4OXbdzv#2KDLtm~lR;sM7UY z?9pyN+lV%PLc_)VUma)ts>03lX2xR1EB+!^6vc^t zbUG>XPb^2KEe7kVqWtb*mURFe)qGRcr$)3O*Rwx#B`+R>&R*bOQ867XN=W8EK@3Qa zuhU+O{beFM^9_h?-2oPmUTvtd1af$3CG*i26u%WhlkHtf=rvz1aRGvf0Earyf~oQZ z=oL{B?wR6CA}TLXUt17a!gKFt|9E}k&l(ND{NX!~4IC@g#=m>em!~hb9I0rwC_1&l zIB@jXv|kp#o3m?FbGBmZ^EC_1T69ACj^*Muht$sBC6YyC7A{=p6CNt=d_Di4yqSa< z$y$_wV+6j5`gy11u|{@7=D8kFedbStV_mn23{(P2#UAxhdAEn|6yTas&^z=-T4g-yrut!rkU* zT}SK#Ikr#dyG;3|V^?3R8*j#yB%v%ioPUq?` zC@Owj24!U!$F;G6gg&fw6QxODxIV^&Ui|ivpQNpR?c%1k&0Kv-L5)RuqhN$?KMiK=^Dh%&uicsHB3k8uDSP_r5eGT8p?P__CryRUJOpjOBY}+5;UDUoWzDEZTRtIhENC*8x`|`Th*5 zGJi#ovhA>a2GADEdfaThZ3#@2C{P z9Mxn;vBub9_!`>EKHfV?lzO@6BJ60_y(785nNg`qss%OIff8tMC}d*Br|HgdbK=p9 zEZe33Vds2E78c0Yko`w2vR?i=S+ltT!pFjt(tu52Yc>so2a+RLg=u+H=hG+QoB8r_ zigV7z_fd0kuItsgGfxq6q%J%5wBmQD*u@3its2wPBD>Gr!S4V0|FY8M{=X5B@I(B^ zYdC`Sm#lx1Lf<2VF<(AJEFXM6oH#BNAEjMh; z0tvf?dvLsc_OYn1Vo9-YMk(4T4TnT%UA8m7AJA!uXq)e9hg;e4mm=gAu659>e!Le7<=qQOvIEI$s z0W($XGK)taw>0jt~ zcbz`@n6@ZtbXAZWh>xcWAnp_YL{qWf4VM(F-M9)pLY<0?c1>RV630)|l!mZ9F)9 zAgVB-?tgBsm6=sBbL8V4erj?at^c*qFEdkbpp{PUu4Hy+LrByNJm7i}%W2O0vq~J; zaxG*STpqwSzOFSK{r&ozY`OO2fQgY!Q?1eVK^v0O!$>3eGi7lm&+S(W!5V_S$X86{ zLrYXm;Kq(`C)zax=5;Vzy)w*5(1iDt*Om}rU@)+xvR83U-*U8)Z780OcoyormV2yI z-Zw-VHFp^0D<<1f>*BC`Sumy~wz}~mfV}V}W@2HcSWY|y7urlmmAx_`7e{~3A1;5os;H8+|s!cEG)3I7}vax>7m;6+s#F%fU zSL1oR;|;ey^4Q$&tAxfl>BI+4XM@p;##)%Wd&)Hwu<=Iz!`BVDxHN2-oW5#Qj_qd2++@P=yDho(6=;rlz4`OX4$qf9Fjf_%2rak@=h$~uLSYzYU>XbQPFyB zsp|-HLA@xR7{n(k4qAWY4J|e*_C*ZIVQQ$v6(=BD>g*+tD~kDT9RJr%Z?$n&f5tV1 zib`{hk#&k5vf-DvCE`53>Y>n*t&wm@J`Sj?G|k05XbT%3w4HIdlXOM{k^7jviu9KI zl<4Q~jR%r~neLJi^Hw~Y7_W%>el@CnR<%uh!E@g-x|J?6S*Pp8gfcqk@0yXUc#FS` z-aky(YSw9!nXrQC#yYq{P9j+Z;D|_X^}Q=fcQEyR?Ke27&9H0!^hTpG2PKWuV_alN zgEp!9y;zIWYgW?-s0%wXtd<5u!g1AuqW=N4H&t^LF}gqY!SfYJIh%%44Sy_v2TKp; zcW=G3@uM<~BUz?7@1!k^e7NbL@5_V>eSvfY6%M9;eqCX`_=gx}Jw-t7n@J(+>UBH8ENkH^&aJJFJV zL}5L*9Eq#0x@Q=xU8%7B7CHSSwX&Zt4|0lxD_9BMntP8kViZn@U5?X=`NNa6Th6|v ztBO@^apVg-JsJC2=Z95jytJ@b20OdtG+VJ}s>NtAEvNGmAb+Ae@N4hS!3&8gw?aJr7X3UQ^iWozYyX>xRLErA&cFCVd!Ef!-w7R6=_E*L8$;koAI)ixPdH{6os$3vh-A+r zzSh*1Q<1HVk-?RY3<`sx(eMf)t*h`o)2+b(CUpq@`OtZvUzIYXX4QYjwwlnC?6{e@ z06rKA25B6_0CZHXSAmsmGdXqM-Q9UUg4deKw!jHh2)HO&7XNKl16-V46<1MJEt;_^ zeyoy5@Wz=T<|ZYt5RNrA*An4ThEF7^qZTKU(v)$WhkTBATf8p4$4Ku_192uz)0d8m zW9ttj0jJtymn@Ck=a`X!>`55-(&tVFm3B5ku1S<$BiP&689EWQ2Dj|TPhxq+mybhZ87Z!ag@ zmrpf~jSi`v9DEW^xf~~PtTE- z+n|*zPqab-FP01x8`nDC@~Q`ZMNSPZ=7xvXy-g``x2AK3DI;3>=RzD!5bJOYRmxLC z)KL}cY2a#Xo1q(PEQ9I*%U&p0pFm+Ze+jM5U_SG>F9{s?(GsXGpvC{gz7Kq2`nTx9 zd#Q90;_bk{@q4PN7|KBK*i3B1zdCzvxLWM740F`vucpev-GHi5E6S^bp$@q;Jy3R+5GNVCsPc#WzF+_vHm&V>s=Kh1MlQ#Aya2bMFpO%DGlpx@qq# z0nf}fGG_ZHw-|3eulAndX|Z^&1)dsr=BlU=eJ+}tB)0MHT7 zLIMACNj6EH-PIqG4Vl;a4p0jI0bV~R9^sNyx54>gl&pVIYaV*y! zG7f*&h#gma7p$N++0-`j=$uh@?fNh&aP`ZBu}c1k}kG8)9(p`4sYE>wsd zzLooLj%=)Eu-m)%^=a>SOEsk&@z+ZVHKEHrlkoZkMch9Q;OwY&SNho88F*;?z5Mog z==Q-+tB__{F?7yj8syKo`_u};geGA&%K<<@Y#nerGs#b@)7mxakK zYob}gel2N&Sb|oy?mQXR3%Xo^57%e!yb({#h?*Ie_N(WTa*Mrr#BdOSdw}iB(PFa& z^bE2|Jqo6o_x#b|kN?+-0n@rL49uLpmn+*Id$j1C=KkrzrHlvEn3pJT;^oZa7{{s)CdFSdA)ZO8R6S7F7S%ETMP{&8^knGOOdyL7fN@q2FLYDKoP04Ur@}fg z1fzT5K3ah?kghX2-y6JouwS>i?W{PO=!O!$Z(?=*Fdf6pu}63O*Z~x76NZmv-IS4z zv+Phg5T?upI5OfAljS$L4348%jXzNnR*#zKmBV=E$#>im!{{Mg6$DZG^o{kRPKoXI z-zsh^=4sN)(tVJ}SA3>;N2QH&_gqoN%(`Ua@qgn@hXm38?~|qSQO2enirA8>iSu{( znEJrXkS4iVz_gFuC3DcTbcWwRW-HIVz3KB$^(+9B0iTjBLe_{Y2*1Gr-TECP@rR3s z@mq1v+VhY906J+gHC%2qo!l#?fgj#q7Sa_oqOMaHJy&!io7?MBPUL`RB;PR|ZMFN{ z@tR;zH-rD@9>Kl_)>^QF3O1=zkZpFYo5JOoZI!71%h6bl+F%N-QSd zp3kkfcp;LEiTAqwqhp3A(h!$FbeFifE45MWXUHuhp#K){GTo|IiuhGV>H=-yIuwJc z$Cr6pF2ry9nV#%{0UsO8YgO^*QBM7z0RMn_Qp13`8~K_dA#>hwvX6Y{7VFArjc3c< z_7`&<@eNrDJ!K2^Jp<32DHuzSf9sMG z*)6rbRi|4afRC;A(JNuB3~7Y3^pzf-o;qrX@uE0?XH}fD8((oC#Dv8CUj>a|NC9b8 zTvLAX`Dca`v4(R^03sm3u$cZfZ3EB9HCurXr=FGgzO6J&<<}i*2cqCCUHJ<0&w=Y> z*3;pfwt2bDvTIHqa$^faQDkY!OYlGhVhq~tMOizQCHS>zF={!o`+{#_YSflj(5CqG zN0!Hx==Xh_!7?FppAqb#$&@=Ijr!$G=n%Z`wZC@2{nc^3_8pq)_k@H520}kJl>MK8 zT(xJ=t|)3S>!q=`cU5y?s8rH5Kgh-8=Hhaxe%0wdp&KFsQU+@E*JmyHsFFy{?)vWB@mF}dhPoUj`%E{%e3#s352Wf24!AW zA?J1+x>AC2@#{Ny8C^pi0L6c+#{@SoDPO?SGIY;>0Y!52?i!_H3PG?79y7=Gvanl&X zi)A>TQO_0#j%f{TG1}(Mh-gTU+3$1$eAcj=bqq4O9Cp4=b^+WOiedN|P?&q;-4YMy zP;+0i3p_R0k4Imw>zTikeT9!))fR_bNJ&}qV(bdp}~R3 zlTsL`oysSVsoGNTKJ>n`6zS3vGh%;!DWpF;OeC<-kR!?$V+ZXa^oTRET9YTF*%gP8 zF?-~%RIVQnUt;|=MTS$otT-Wizo|BI{MKu%nM}8?)qaP2Fn(LS0}8RyIvUsivHE1^ zRQuE2mJWjkFFjWZ114a*5C8dsZN03)86g$e2{mhmZtXDVf%J@EYaqiM*^aSHB+EHh!FGSKp--?+l#{^SzZ zCf&NhIavM0C3Hpi<{o!>h|u}-39@y*25U4}8>H@0S{Q3^Fs5qt6GG-O@}nj+paUON z+;qFSqW}Q{K5sANe+5Zl;i-=XwyttCl>TdX5Tt&T`p~L z@-WXI*R?w-&~xBX_ZoXP8qoGmwRw!_7KBV?NnffM44|lgu%?AE=7YuGryyO zKfl`XSM`L1B_v&Af|+>GBVoRs_Hswz!{V%?Gxl7Bi^lCXLFj(sFqeB$Ah2^;@7VcW z#azDd&K_3g$L&SpIzIOeLMVz&e>O{r>+AenTv$@&7ZVHCS!Hi809EL*k=`F(@3a~9V6y4WvW8>Lc>jg(@y@iVo+ z>yws+m}LpnxA{Lh4=E0qg_xWd&b1U_LR)Dbt0A>N#)_wo`=L?*k?R|`eYp#^2N3}H zKeKvQA2srKEsEblr;;}_WG0YvH}M!x)dwrY;MJ+sR$`-=((2df9L=V+-7F7q=j9@6 zx<|Jvoz@~u$yzh+WPt;CVVdx7-hd$z$&3)ayk^bQk708njoSmSM?OK2@C~Peo>bFB z;iYPl`YAOXE%^{9(R6-ey#I)_yYcmu@~P;?#PL9i^?%G2FBV1ZfjDTlzVNLV z5TlVFdi41fJcI-#=eNJy*OgNm(rHT->$Y4HD{+G@r2>Ed=q4+mvZ_{+ne2899C5~mu|W?6&MA|SU2b}MFLlQj~fx^P+-Q>_>2 z-(w+%cVw+Pm0N?t$wt$=(KEu7g-5j}N*9s125*aEU0nlp2Xrp=$*|gAe(N!N!*d5W z$0R(j$}LZ#GIj~$cA3`(>f7x{>b8)L`nk^M0)C6Z-MGwPxcYy^Bl+kZ{@*n!r%6KT z`hIT&E&RS_1}G5~d~M)NWcE%@|L)uor@QOoy}qwre(!3PE4fyv?PQWx4xgq#S}&vN zXkr+s{t0$OcYYhqiALm5_4yZ?dP|xU2e!}G`jMLJhK9+3mq~M+#kX9&Hf$c)J<7e! ze|x=fjsCy9*ocsOYvi?&P_V;xnY1&3DddI{H+>+6Nuw=XWCres!b`L0#tknzmmg*k zSW|LGiy#w*(4#y1acSfM?z*~_$jE!{=`O)I3C*O?(#-ch4t~p+xwf>F=B~@3>lJ@QKAvj0xLwQehredm+OxNr#}z; z^qLtfpJzWX>=xvi>k3l#{0UfQXCrF2%(=c)`zs%lXF&6sAt=#1-S z7VX!7uTr((fUaPHQ@&8iBm~CeD$>W=ddi3#*7+^$?UMA*h_HptSz$@a5e9TD8Snlt zk!Wc-@`EKhj}^UNEno0U(Nw*dMWLCmCmf^98FLx+4e{41P(cpU5I$cE5mNW|O2NWj z)~Lqzb3^rAHm8D^C8x)V_haDG}#Me#-4=CwBLt5(2CDqmYI<>~B)NNimM zVv4B87FKy~PICT75FkXdpBFD_A8BI#YNJg|p}92r&fZ|n!yOfXhAkw77q^nTV6e3yTzUZnZ=8v}GH)yiLx~ zw@JY>4EnB#)#|8Zb<-5||#Bc9J_0Y`;1e?{I2&frOE(L^&*`JHd1R7`U8?&LrJ@nd#|MNSk&H?xL^ zwdLdMXa@|W)Xvq9N*GPg*EHWigiCZ2_ftqr0jk7aB}94jzH921 zsco^lx~8qVl;b#cwO2x7X8oZfQkY_T-V7=lnbP;?aR|#~c~a;y!{k8sRN`GvlGD;o z^PzxhiDgTzVBQP`L*TQ_%3h{15{BiAmz5$`Q>?sD?oZy;_oL&hH8Scf-p6}oo2kaP zN*r&~r38hPhk>*>Re82EWXxdmru+fvrPK9_f64IoSAqxaIX>`?&v)Wn^xj0XLKB2o zrDr+Tr9$`&IdZ5@?v~*=@Y`?ws4y$Kx{8RWCJ26E2P+1@jh9sQNd3a{-uqGxu1j|{ z8GVkiAuZg4eza@%PKw`T%$=Fkp95zfrPaq@lfc{HCjDiHBX4A*^-Ls6Q1rMh@Eb&- zro%lCi-zb|Dn|DAYTz851wja?_D@gx1HZcJS!V| zC&hGdTXMugMQard4cR5aGQR^Ol$St3z-pq7TYGUY%45elsGhdlw#gaU_l8cRVu0z? zGq$nNkng9K(MqjWis)%VFKoO$DNtk^e4Y9hZB9=m z88E#;mdqu(HBYN4d*P0(m1#rpnfxp{NA>HuO%7?IIsr$Pc|8;j3*Lkd`!G_;(cel| z-Qa{1wD--8=&V zZX?Qb)@4vnz5X=GwC!$n*G4@dph;00etU|ruc}jxn)>< zJE){%@KQN&XvCc8$2IdDHw%%<3cHJp=nhJpe7zIM+ z$wmqDkn;5Em5j`>qF=@2Zi4E^eJ;Ft-MB4JeY*Uh2*#|@bYm!dbzM%o1GY3&9mSwq z-rSBEGCnORndx{11VEZM<#AQIH~HH$CEbmHzd z$A1BvV(*rVC>p9h==Y(bJt2xtMbJGH2$r<-$bU|bs3=3r4uam>&aV3KX(4>j`@{ZV zD>yuLFy4(xlHEY}ikOatN5?kG-vLUc3uF@QfnTb;kJRlHaB@M&t?muTqe)j8Gj)|0 zkOiEEv#ID0ku7yhOht^gu2fOdSC_eYmFGHd(cPk^wJxXc7e4c(Yg#%Vo~TtEHwkQQ z?CT0h0$|JPBvdVI<|IGh5arnnc2_MH^Og@XW=&z zxkv9+T{}79m4Mk)ja-TnM+cr7tdFt{#rA&pT4-a><*9u`bF~3o#vmv-h8F)sw%apV za>5TG(cEBo;r$pE&29_V7D6R{dOXejrQQexOV!;oH=XmwOtla%@Vvqbv zP|*BlOWs<}cS&7FX}-E``E zpYa)2oQI=k!j|mz>}pUJi0PR>3&yiERj8GR$ZnG#L8uv&Hh~S{$Vy7HUHU#<86}9p z8a3qI%F-@#;+xzfhD$v~8fja3)uDO12l6OuAZI#4z)J98=3Mak$5xZQ!hgmULD$bQ z6B`_M5~u2|NB|3AmO`?dHimU*7D#NcGO0;g()od=akOISUz7Qt0eh5dB;SQ%=5AG0 z^d^~z)s+e|Uqz>6Lch;EPAIt`@adhkWwZ+aBX&^2mNzlEx#l*XB}iR9J^To`h@Uj~ z*b5dW4{pP}Tf7j3pM8){7v(H))Oxt~B#mmww4F*m&+vFPTr#IfD{+x<6CS{J~zLz2Hm%TT*5y_f zOV(lj(41soTs-OQ*`^G(k-|`S3fFFkNUs}Li*ISeA?34#u3tD%vAP-W$Z0udXF^If z3s7!7GxUz?+XfFH@8#~rn{?>Of0G&15#=sx*oE-c>}3)ovit}sDJfq4Jfu2e+f4_V zgQ^bey%K_KSGZw<%xI+&7~;E8LVtDtQ4Pv4;$nnj(#E1e>_8V~45dzE*U-`dBeT{3uSQzpJFkswn1G4GIbErBJ?66v6*AAtYwQ$Pa3(QDN;> zRXNc*q@|H!*9M9n7NV2VZ_RHVR3@GPT6l#!k+Ip6BMaEb?&bk$PE%V9IG~^#7Vxeb_&*p_@(9z*)Hm zzKikLj;U%+gv^_LEg`#9*t-i!SAfT{smQH5sAi!!<4sQVBX!9E0MBXGAJ9J|IQe%G z!ar1*PJ_$t%+{(DHmiV)q?_fb+;~tvUv^6FoP-Z3K>tN1iQJIXdeX`=V%FhoZV6K7 zyl$-qvOh>0e&n3U|2Y}Dw@vZ!ssC|Dhk;IsZ}=A5YAnk_N+!-_mw=l-5qH~Gpum-r zxSAy(zWTN#RSrRf3IYgO#3#LZ1Hts!9_b?n+xikQ1q1&~&Uo^_qgLUuuD_7~w12nB zz1>a^A0MoFU0n4Yzc6=DE~Tqw8bWjKZDl+rQq?^qF~h7x<{C2o)r8B^<(~KBRTGG} z?*JwmU)@Tx>FZ9Z(6L7M91JrL)i7bDv(5VaO=LB0?rOlG8cO1OtO8-;AxN(Rl&mjD zs+*oQe}oy^jTn;=;Lz7=dM-P{Juj*s zkw}h27m2jaiOjo{wlrNLCSfE_KjlzAI)NB`)Uw689X9&&BFItu;ob;+6JEv6z?^n zgB9&X<+y1%KCb#@=`0pw>g=Kgfx zkK&}T1k&N5pu%;8bv~7<8`+MUnnU65RN8I9o{!8|7d4>mU(+N*RN~<0@bV5my5mj0 zo{ug5Y`ipU1e3ecwTVpVRnK%xX~ro1>?Vx}52z<~bJa#|UdFPx55JUoA=puh=r?Q+ z6xmvi#v@?%t-{2kR8wrK5{`jU!>XV*n}+TP+Bv7sol;lSI!^HK>@+NpJ?=>v z_d9OG?H4OrL`TC4`SVlN1=ai*r@C3=M581yO)bmGe1sQzXx5~Z=R_>?D|AehAaFYh zmQLSFv0aq$%OZ+DBQrXJ4OP|a5^v;I?L~&|t{)z2KOgE;kj>Ig2}cA52E&LLT@s(77d{P@4*t&c zD~>}|*`e3pX`8fq)|LHs0V{B^c$vjQ%3|i^>7c zZG0Fq%x;XtYFD@|>ypY1*%i^TjidE(&6`h0*S^}tZDn&r(38biz4U+2HvDZe+Jsc1 z3ZwVjUoe#~^k5QT6=k{aXZn_64Uj&FGkt?TK9Mlv#HZYZrw$0u@62|jPcA{!=)orI ziksv5!g&TJ>yhy7H1Np+#zJ;3%#iAY_*{U?6cbdR(Zw5~Uo*%r1m80v@!`{h>vJ(=qqV`pgQ7<(vFAb_(BYYI@%(pta^&S3+4~vC+4Uyi0lw$e$Il}Cn98ek`h)y* z4m14uSoJEvjbYtuwX}LWZy2I%gO-?M9ub3Si9X z`?aV;j*y(Hkjc)fvMmlfLc~Tfi%zuy+8z8wE>B_vyS(r4ncsYRGBrsg8at8Np*?pb zF4Nvf5PZzs^(mXK+-vNA@FVuR|ARp1Jr5mWUZ3tCkRuT4=>?PI36r3p1Bb2umZX3c z{>!&NwZ|;Kx`O<8jw#FE(awAwu!T*Zo7v44Z*Lv8U3^Jnh27s~jgR>|z;a4`IRI036G+EajXCB$Xqm3RZd?M!tl?tSxu)XB6hMjqk zpdLn=SO=V#2MMppzYVUAx(<=7D0aEof7%FiCUa@`sI>6re~gp-;0iBZmnvfq4XAOP(Bun5Dm z<>$*hpHTlCK9`>;20=>jK*m{c=`%uiPni9g!)g8-8QH#q`X1>s1 zXC?91MLa>MdgS;pcwQadQEH7Q#ccwNL68ssC%I9iYN+N3K+^;cJ&ONYtsu9uEz&YM z$Wc()lDPJUx6IMJGE`q3YiSM4IC3@E9Euuzfnp$-W_~Q$9-jQnQdC*61S*{nA{vylrIA+W+L<{1HM^$h?%H;+n!WnAla}s_)Ln z^&eNK7B6IV0%wztHXd*A?(QX>J6$=L^c)q#W(yik`|gPaUvZ~ERL(SjJn^Z!st&N? zbkFL8xD%<}d)@t_Da7S>NpIU-FLNWpm3#4jO@rQmVzW4XQbFeAO{?bNJ73rQrY4}_ zY*cQwNNT5RJ5S4&H<%iVdRfVTfoca)x;+JzADk@^r##M^Eq7ns-*@3L;XH;tWL^ji zGKO6US71iKb36B}k=0LZ2WV$QE)~eb>0PMPtl6{APvJ;+fE-gDYJh=xI}US7t-XB4 zk^E>@EOgD+dcl`_fE_s88v>N@C7-`+ttDuF@3!!1Zql4id?0>#sI2}dV{<0dJ1lpm8Nr>40^i@%q_4H0-`Y5FMw5+P0JwtY_2gG(gRudSX$8MKPjn_qQh^^IM2 z)SzwrmUC82GrG)<=|H324^~a_g_2xAPITW1wXH*y$%USCI@tMQZhR|QFQ65^)DOD) z+#+8;d!1_v(1==Z`?3~g8v=?S)U44ibItA?ig}cqR=Bow#T5DT!^j?BKA^~Hbu9?i zH==C)!juP8=$sR}YB<@~|8-CsFcB8yCXJ971;*P(S7K*NBC9AcHmueraRAT-vq`q) z>{&W8<+Mn6+l1n*DFkScyd3$?uSytAjPGMSEa*&T9%Y z#|Tf||HAJ6bxyeZ=&*U@QSd!C>WDV`DDSo3cZ1JdU+>tLrV-meOM+P2Z}+xjIX0_gmKHs)P#7FO+uFuz`Q@!=hdnw zRos|W=Prf%cADcg`CPZ50x`f7&PhfoLCY-lcKb~{Ly65UHAle3@cl!qytSVPOwqHp z-}y6Qy4rn@;dsRVyAsdoBWGFXYMYN7zpHWt#%4Tb>0J(wBhd^g1R2opEP-e6jcCzD)0Rd6sFyk!N}u(T9^Vc}ws%{S+Lx491K7H#Y~FV*R9jM(1(I6p&b5s zJWi~*XfivQXOkAiLU4(M4!O%ijlw$J0@L{;-yt$IMr&@ zp%Uw-A4LDEz6)>Eei>qfQdgpzvYXH5;}-D70EAuIEJpky6r*ZMUAdT3VoDX`pgLr9 zZ1*rHkPXjfXb(_SK_s%gV#L|yzyL>^|H(S5_?xTrnyNfk2>*~@+pR%>6QszH7l%W) zkV#ZqibQr$8Dc~g%A5MPEQLEvux%h?GwNMXt-DR5Fq_Anq^CqP8E`5%3n-NkF##Y% zU3*cAnM~b_XaYGRJ^}+zl%tnz@fw!f-PN+RVn1AA332I{uTA2vtBUGP74XcbUNdwl zxeq@gT{kpJ&eSadI9<@m-v~nRr;t`%5Y;n}+3(0^g2$XVp$I9ne8mBx>%7RG*m62r zrT%@s61VYP&pfz}kK1D=37R&(JWkdmC#5k{W4LGd5#CQ#H(?&@=Ps7eMX)i-Y=FYw zr*_c8C<2EPBUFivNN$!L-sJ2=>Hv}xhj&v|X5Rct?VcILs9g6|C6lc#NVucf z6w>}8uR~;>VlZ;}BXQQ%{XG)dOCpZUhfnvj=9xzeqrk2 z|CZ0_GP^PT;4oh=?J)Sd*fn$6yD7Y}slu?c%xZ^FclssD$C3{)P>e{gsGH~8qBg-3 z8COSzqf|gX9;Vb-!b_4XoSvAPFk955;X#p)@CoPh2e^187tOjMB{U(e|9Hy)Df&Z< z4rS#kWe-U+mEY9upxoCWRWHSMPvBPk|l1u&IBai0tTPhMKnXp+d^q`5*! zR&bWYF-sk_nOMtLj350x1xC#w=381GLtiFlMP0z5lW>Lv@eGn_;mZbD+bN%zuNkw- zg;wB!oA~SV=4k;lquTL+GZCZo({A{q2FzxU^6rL0)*hkH)1JR&_893epJhNU#BUiZa?Z}h6;TD6liX$xEOv8C5;=GmjRxi4<2QMc)O zn!6%RUAH<8BvkOrdfLFf#0=A#)@m{j{!@0`bGnT>vR$F`4^A=p=B;#_myym9MyfM1 zKbw<}Ve&8;ia5{H$mMpm!a!|18CYag(P_KjbC>ipK4v;rtr_|lyp)KHq1_~b?@ zV1yxJRG1fI)=m%T8m{nhysgPlGI>r{#9~`pRXs7wh=Zu?RV9-8{(>K|zX1-tTOD1Z z_W8@S)AR_=y(ha>8BzKp{g{~Aa`SfizEiP6>ZcvII!{!I4{0z5Q5fgX2G%kesNNfV zr>gWD{O=TYX%BZ9*^fDc;cY6&fTN9R4pXo~3kSCZ&Q9EA9ltn*r{PhH<{PBB93Plp z9l*D$J>%*nAsW}-BB?0QU21}iL7@~bD6Q2FN5Es0eP z(&h)=qIb|LZ0M0!stG{bgO$tMGd^R2chH*wZp}O9N@!uX09LOb&3JzgPu3loL%#*B zlD@zNPxF1rjaPKo%CW6Eo9mZN(6BwLIEPPInMRkxUmuILf264V>vxd14RrcWel!2N z!(X+!L#4AE+sEDO|Fa@phAI5klQjEMyMGR!-cqnLZ4{L_ti>Z#(!Nq%4yK{(LyUUs zwj>90YKnhpe9G|obbm?WykPci5tXa0-&qjVN+DTs2zkF=lzHQO|K~oTL;mu6PR+U% zSYqFuQL4c1-1LZBiym|w;SCRxd?QnSM2sJ?qS{j-E z!vdfpP;^<--x*7w#Df&1>`C-a>uP&N74J;S@o#_`rNidycXIiuEa$!jcSF2M9X4$Lph{zTr-FCQTy=R18VTP(MZNA&UxG=z0n_WSwvH3sr90S_bfm(z zptBTx%}E&oYXiBhRiDWj+U^kE)Lm2yh~@InPQk_1VaoeM@suTSys5Z5YVC`KS%XImh1OW~~OQ9Ci=DxQyI8-LXw8DcDlptkw$z z+C<9rBS#}#(T!GRucoQL zQe+<80` z;8+a&o%R}A>GdsP~`wH$PlYJx22>6s|qjbef6oHtwsP|?Lf|lmR7wH< zCyN>_g=;;SFE(m>Aa=f#Q=zE+HxSpjf?LV?MpxiZ&jNdjAQEIhmHyr4xy&VNcB$C{ znh9a}K1n;QWBPUM)HZQ=+&a3=wChZ9<0KKeJ+04z+2>>ib{uDCp{|P{^gyEi^T1!i z4yDPW)1wqiK}?6ll~CTlXbY~1{NVecY@`G=ie902?T$fwnUhR3sB1$nb$A@E0$nA4 zTnJy#fx2G?#qks{0Q05rIRg#nRM=Kfq&mT0Dy0;0z66TgwoxP*`K8W{Vruy+j5IzH zbzOMEeO++BrXI^j;Y&sIrwm-tp~#{JEG}?lAuwzhdwyxwTAKo(!&o%ndu6!aD7p1a5UB|PCK|1@?z+^aDO^ts+F&6C4y^% zE+&+z{{wwD3UjOLoTK|?O+jo2$+Er;U;d!3s}l61l%i6K2H@vbhX=)r2d=&vl(eWF zo_lUbG1Aat!lAxf-+$bXIiL3f zb6)NX;~&(It18qWbDpJ;JUOY9Q{MN9Ar4amR;C6mYs2KP^sX|#)v*J$GPWe5S;lPk zS*bTbt!o)UF_nSXf?6xs&<7(JN1~W3Wp_DbF(P)jeTAod|ALO-v z)_|9(htrg=C(4V4ZVQQN016A20V>f9d`aHxmF2LIv0%>od(_{X9wT{N7>`1T&w+*- zAL25~Z5nc0___UoZ) z%plsv=C+K1f;=V$jX0S+=RN<8QpmtKv%A)1$lSe+3+RY$WZdT%TqC+deXcpLA9A)$ z-$Y?#oCnkr)J;Y0%>Gdar4@L(DJ3tNhqb7|f_xPDzk#}P zctXe$9hFAr%C0Qf{=N>l@KY1^0P}=!a3LrDNL`-WTy=oUelGG3?X7NBnl% z-yxfm6BXhM?HbkSlH0?Qg3UU3420=lN3H6JC^ksn1 zI6;ihh<>E;dO{zL*9xBb;XcK&YCNMhpd*!0fQ-y@`C93Z{3!JQWkHHOokyM(DYcI? zA`Wvod0*8Gvh-!q+PHT3mu>F%%)j_9%pri|OC#C&N8$cpeDgtwJ{YLvxzo%G8y;kd zYk`y(pMm-XaMK2!S%&!z%5+(0VaM}u|IodlsSDF(jN^@$$U*2!ov!1h68wP#dqLx| zFfQv-w^hFE8(;F;%Ck2WTuk%8I`KeASf1I_)^({Db1ke%@WeQslbCoarfiyZ(!SLV z1=}#8kY_^6)Li&<9E23yc|2Ur;|y`T0b7Y^w!q$chF*c*W@y-*589`d)cc-sy~aiS zMY`@w8WOst=Ye5+Msu8MZ#7a8a-}+CKJ9G3jxKjnCv^HH+&{o9;<+}7M zD=+q_MJ$TM7^EJ#QT#jan9kGZw4?KaI(gd+ifpp-dY@pBJtu-c=IWQm=U}~5z1Qas+dK6p>yrE99pXV(% z_zD_D!oPt}+L&v;EU!EwGfIKvInb}CxHbXALb_oLv9WDv{VK!)YA@KVGt4_(@rS0& zC~;9HOnpkkqmsTR8hLLNDa)e7vs5~jrL1u%C24j+K8inpX;C@9mh#*rW*PYg#W3*0TEh;z$2~Ub`^$y=gVNydLlA7ReTVAR z@oEIV$s?GM4sx0Ix#^W`=kxtw6|-1 z4(o$B$M?95CC=12H~eC3zT3Opsq@)xU*|KdOY}TY%jrX{ZqKmJa7lJET$a!%}q>FY9feG&y_ZxyzsLw@R32B`5;dqXB@n? zs2MNY@GU^LO}U-V8w5&fi#RjPVH4+aZRR_yo6JfcL#ME_9F~5!2={AhGUzWIpQJ|O zjGggs>h0%6_U_f`)_KcxOQ+4?ar4-ouc2AL=%;gba2>g3&g(Z~h5ng(1GvDnUph}7CXYy%|A$|IeTQ{W?JyqFW}sZJSB1UeYZHI#Y!d&=rL{C(o@6*p^rI`R?j zCyGkE;To@k3+i_B9;X#=+IKcZi?y_5E_>s*SPm)+${njVWi9_{93;7h#Yt#+EbSZ> zEuU5G@_o0%CUV6Fa`kFM$W?j4SxQkUm*WNDU&{Q-`OA&~H!au3liet#Y!j4H^13xe zffx|YY3HrK9!e=H<@&f@tA8L-<;DH*m*!P?QHHBxZF^DtZ`(ev;XH4-qt{`1&Yzdh z+rEh1J_gS=`GF=ssGP_YcoSfIhc^$&^Ds%u11%CkincJ{%EZSORV}vCM5tO%gzd>A zH+!Vr`Z?@#ncaHB{+*idu+FgDsd*aWytqBvI~>ztop}OfY6Oc>(>dn4IY0NwvArPH zKHliXG{%-Ydi$n*Q`f0|!ui%4IlfMtLDLO7w`U}epW~8eQ|VYE@|Wh|D>dY}z?M1D z+g6FI=BI0}q&0ob+h%ISWINB3+%pVP)qFz!Itb=K5Yu@t*doWE`>~=Y(mukE3!sKb z8q_6nh?s)Si0m=1a;o7fS`ZZPpdyI&C`gqmw~wqpm6BTILWw&TU=t$emi( zA=-=I8hUN6kJ`D<7&e-O_uwMT+eXaOx@7n;-+`E?6bmTnpD}sL~h!>W!hXgg4CR+ZANt>u|#8QnYbG zaJ|Mr3*H3g+gI`xWa}Yc?@2ALyBFcQ=6oqdrIfBp-jXq(=S*F9u-y1aeJOZyGAJoG zo07L_z*b7p0E!w-z6hxRafHK@FGwA*&!4zDq`f*nxe%B$6}u7$tybn$i^s_!;$U9!DLaOF zOy)a^3qxnxao%QmV9W<^1~&tn!I3wBqgH+!I7-9dd*WEC&$Ngm?dU`Y+$)FFa0Ao9 zQPuvljSjhA4*1SYurc&J*Q?{z2z+x#fEmu0Kq+P0g;GxWt?~@>BKsNV-M_{YK)Yy=_+s%jCf8D3sI(5uN{6)ICZ?}ID>NsQWn+yM0 zt=;BLl*i7peN2rxZ#lt+E9nJ!=6bhnSm*qPc_Z_B^9Q=#x-Dp9%hS%5pY+vnsb?d} zb*L;70@?b?FUMg&;I=9@CWHcez}E@Eg2Mb zuK)lL07*naRN=jF3l>z?i<*SaydmT;vW6CuxsjQ4GF*r2_TM{ewn)94hnfZ2TcqIa z&v{5~@gLh-?dv?&dvj1I-Zjo4`e;~ZYIMHSt&aN5vvSn6FtwFAw!CIXOuWYve!4$B zZj)h4=DIoG9p<`Io}B4m3a0$2n%83!vd7D{+AYx>U+R;dk(sPHmf~7TV$OJB>+9v~ zlp8{7;Dw1gycslHlc5%=iw@g;-5bQbP`h0E{W{95oX+=gE~RPwS=?8~W9F_>3Sv_x zi@3}v>r_42T`Ud{}yitZ`2bVu8(tWw|>0(yl@0cVUVxDg+Z^Bzq1kW zuOy|EZF{Ad_l+cOcJrDYmJO?*=?1lUO_WmP8m@y<3N`B0am5Ja7bst_m(eRi({x!} z*Qe9(g61sAR5?@E83Ma_XR( z;>vS0WL?_v>%3zV_}iw;8B^|1)=90VhV7;XMm>*;j5!rxoo2i-_#6N z;6sC!D8)8ovZHNw7*6vYhb%f}e_`k-%eGJHf{+X82E;lq5VR#ONeaVw0 zK3q?4_(GmKOF6Z8jpEv^F`u9ZmVHeun_4zMz~si79I$vsu63GE2=ycjdWupCa!I@L zi2OYBfvTXm7$LWjwFXX!5K|aI*c!OX@aS<&|B0UR!llaJBi5ZlS)codgPk&uxXuYQ z*4w|+^L&%B<{Yd?mJ{NYJK+M2N3{mc(k zT{Q9uJfBZCDn~~epFGj>=_Bp2DZInm!3X7-4Pe!`c8b2ex6rruSJ0)twY${o%c6U1 z7Vm1UTXC#GuZ~wE@Qoe;ZW!M*z&E&W)Ixt6FK(nV!Tebu`QFrZk8<6>Deh(Cr!=p~ zAIJ#IA0}UfKi}=kV!Gc&cIyn!MN`B6;l(ZwG|NWX(U~{WC+(Fx4|M(v*lcjMVR}a=N)TZr{=vW zsccMwS_G_1JDUho>pChMCfkFJN(I3fbt?E^j7+y6hd>zED=|hOS72($?IMfom0;Xb zhu*9a=JAOsM}^uq$ZKCEt$aTQY^R&#BV(-(ZU49_eRhAUb54AicMbUW`6KgONarwQ zAGhVJ3Jp;3F%`Vw?{rg&p>zjR5B{l}e9Sw}`MiNE){{GB^PrH|HtURH9rgJiRWNKD zQb+!Y7RS(*7hqnHCaA*wab2c}dD>>ix-BTzdZXJ&mtC=snQX)Dd|5bQCQD}epuuyx z9LuzF&C=F=OsRl&)67TAc*H#Cx;dZ5GaoG<8E4vH!D^riC9gb!=dzI1+~dpBHhzRt zv)980S5BQ)HYUafnqX1Ti{=ydN z*X|W~X_7S^0X2lLGDX&os>LYvnNv!>^*hJ|5PbhQu=^|9ZnmzM?`M34&6^&_H2>yz zM#jfB`zCc9a1Cb>6a>;1^7ozFGx~kfX!FTUpywDH!3`jS{sjG~34g~JBaIm5jmvn~ zR>ncYFlr>zlJC2Ho+-EK>s!lF@9i!1(|b4cr}qx@CwF$ij(&J&N8h`>)O)v9`tGeA zefRdh-o3HMo5O`(UzArHLcKb^;Uf^#e-?aKp}Xqjj^hUM=T%-`#O55=Mdo+u$-Me_ zH3Dq}+C!xA{qrJSx1Fai@;z_&{I$;12o}kvJ+FD|?4x{GVuA9>=?UC{$ivZ}4c43E z(T9KVph_vJZ4Lg+jh%gPh>3EG%CJ3klit48iG$31@(8vKSm?f;@6`J9{F!oXJcsi! zbJ?zEWZLht$H}}Q#?$SXpCiXL$D-Bm80$>ak2q87OszL8H_9mBl1RaQ(E{GR$Ob~M5FWN9K^g<`=OlJshC@o{_e8!@4+m10c=q;Bu63II+ zaaoi3DaQS5XucVXP3gn0;q+^OcUZ*>1#f* z7m~~D-h6Q(m5>kTZnQuQTE`qfEPM_4fQn7VmIZP9E!ZlWzN)Q(7>Yy(BhriIj-)2Z!~+DdLCE4Q!3B<4KS^<+yngcaP-T-Uw|)zVHo&@=lXb= z+P{iep2>U->e?vf^xF4&=J#CJT}DeejW6YSem=gXH2Y<_uA{FkE?)-oHcM&sT~u=^ z%~r};dQsaRzl-!;H@9=caQgn?8u}UF#WY3P$H1sShf0NyQi@9HASV0o`;Ff`H*0P9 zh0<8BPqbdI6+9fnc~zMsvAr+tlOTSBkFC`d+5WE8e|OL{L5`#|yX8`PEsauQp3Y&U zF#E91aBN5LM(Z+E|3fSjy3bt4H;VfGPY1K5pvIni!x&^gp>w?$Q~OwFd#4_=#4+P{ za-uqoXMgMH+6^~U{K2>|$Xp1nAvv9QL%ls;ZzF7*e1rNNlXTLXcUzTfXl$r=?f*|A z?N>H?**?7U7p{{g1U92Mr-9y!A_`JcAnTO&IkO4F?bHltxe)s znTwjmqN;8fFX|RGSzqBxuZg~!FZ4ItAh)ToZ>f1;T?6x#vCLewF{c}3qt(yYlV1}8 zYKXRyB6pj0iAfhsnrnkTc$`m>6T%;Wn#f&BQ7H}0k5AfmT?USMmQs@X8c6-CBXr4*3aBpQhPE^j`*SBp3pI@2x} z$D$ihay2w$jzXRSFQ_!AINd2cUuR&Zs`@_TKCYfSjDJ$kALc*U37q)Dbu#UyNpVl?|sAd`FqB2UfY}_JVtEd?6cu} zDa1yQc{HLQp$mh};Qk;TQrnGj(9Ok|FmIh<-L2(9hWUdc?NR57@4w~xnT|jImDXT! z{Hg9PH@Y{jb$jzvKfHHS|LcGF*ZR}f@4_GIyEj*Q=Wx_p#*KyEbz5q@kzG5yz!FCq%Zbq+;>yAM5zyfm}5ccZ)YDOUZ;F?m+ z`AMa@uJgWR;vzN*!-*rO8F9ae2P5z~<PD= zX>CwYop{tpZoygu-Zo&H{Y)L(sd?jz%5>IAFGco|_B!qUU3Oa?ZqxQsib?@=h|g5_ z5BsvF!VMz}waR8xjMsJ{0Uv;Z=!IX=!c!7=>0ontoV?VEaHTmHy}UgmZz`OOce zm%wdZiqJM+>qBG>Zud&iw*4>7OD$jArs;^Y)E%}a2lbrI`fMYam-}!##+fX^IG~`P zZoUCs$`i%>To}49b%q#!AT^5Yadlo}q*MfU_Obi-_fqoU52(q{_A%8s(hxylUv%cLg!652MT~u>d~W9qxG;rTQgWVcG&0wg%(WeKd;HLF zfc@5@=r*{+TRr3MYLszrXCcFU*IPF3?=F?t8TGB(J9@ak(P2E+&GJm|J-n%Z{p0ub zm*4-c-h1t?9_%l4Z*LTCIv6+FFDJZBJl3i{(<+|n#;_r->LxWFvS}SWc(3qk1ipzQ z;4d6;GhSSKU^1|J;|HsxM0JX(@D&4jb-o&b-}?~=9zHJ8^gQPJIp6)}x--0UvN)Gv z9;0Je@hH%3d2Hs(WH)~ixarjNwNX|17Hhgdhpa|}MLJ$IZFzz+1Aa}eY!1Bfi>ZC{ z#sZewZQz^w=PuO4|)&O!&gE?+A zmH+?{07*naRF<`2k+3Rn8h(qoVZj>Spq)eN@H4efhu_+soE-D! z_9S6_bu1f2Zvve+;dTr*fv&AWW0GrbE;qTk^oBB+Bks3rmH7_Fz&iT}u|_pTpMF#S z#2g5~sBtnI;0yCZ_}XTqOq{7Q@hL%`mUAb7LjxSAh(c#9>dlErtfX_(>*G|iz z7lJZqAChM&Gec^=;75j$*MpED(fWvNQ$yDYTj*tu(|qmP4qS6zS6|b@ydJsM3j=-$ zU9vIsVtkkHB>(Zd75Y8yzdh)#ZBXmlAJ5xpL5wjnrU+R>jJyw~d;`v$^g4imsR17oq?tb8CpkKeNvAy8{8o-z z%MDMFS%cc)I#A{9mOMi9o#hJq`r|B|$AP9DbGh-)$F}@Bt(AMh@%A@5Z+ZrkpXXfH z+dr5$!F)0XHhu&1ZNWxx$@l0U`WxtOF`4eN5xhn%5&=w|&CzbATqZ>@Lktn}0O-_l?G*$?&JyRU1xtlDEkxU;v=Y8UJb+To4h9vjMo zgOzUX@8|{_(HqopXiyVP>yQ3;H2}ZABT!0FDVM{Qx0_K)>YJ4B3GORo>pI9GJ*U&F zu=>opN34-~070!2U?rYm+@_C#6=DIm=TZb3za!o9PtV`Z9 zFmJo{7o)1#E3RyJ^1&GBc*`{Lj%z+K`9PUH)*H4}+OpQvu-+TM>KyoXYJEq>u^=?H zZcfc#2;N%73n0e9nEP#I>U3Ujc8uiF`MW2Ga~+1qI)N^Is53zFv|m|>CfLMPW0FOG zjbOnLlP>)xJ~wO5!$KqNonlWJxINnrdpqT<%Z^3x^8sY zZ~Et&lr+@Z!6*AsReLT|bj}-4>qQ_w>r}eoJBugJ?8x`KcejNqsacriS|v8kF}3(pZR`3 z>;e9J2G?hY{Nu?+Bln2&dSf_*@{iRGet6NJ@->2{rz*^y!ZmrgO$}Q*HE)~Y`>2#6 zPSkXa&r*sn3Y=13N`aoo%qGcs2;z*~?1`f!f=@%}8^vks@B7f-l?{oD|+)m@w>>1a5oYS@~xbCjOvZ>K{al{$e=J>1uljklsbhhCb zF$cXFTr&Sw%&7y~y@B&>cH8$CqaH37Z04i~3+etC7vgT&$nYQMG|b1{XUEt#+2$ z+g)jQIck3apogoa2737Fd^G}p03#5s52cYSJ|~kMjY)h(t~CN4^_Ri58hsU%Ban-1 zI6sIk0;W7KvRP-Na~PHlKhT1OQ0jPynHs6*VQHTPi~PROw>^01McTdK3{`#!^~SB) zf^h2%^9{Cb!NamCp=+}v{334*%GJIVe<6QoRV-4aG zan>73Kik}%Q-{P@ltl9vIS2ad{0Zb^CjzFQouAXrcb~69XNmo1Pj&G0scsEM@PDKe4?Ar zj&<{R%_4iFL+;6gb=4kpmkr_q`*5;Wf_tefx7iHd*&OLE8@_k$ z9q0$|zM((=$@lc5AHJ^#Z#>X&u%mMGKznaI)Nt=MxFy{>)MkHI8#aR*-Ue>?0^IO@ zJW`XfS}e3;gNj}^G@RkkOi_ZQ>Mz3(WDD0K)n>Yf$ z-!=Q;aTU&2KlioGa;N8c%f^fB_Ax4pX4CT!4>aqxb{5{2PvHf7um<1Y8Z%gL1hz(d z%r@7Q+xp7F&a|V>ZBuRYP2tvWvN>nR+}3s1q0ZyDPR$#Z!{fjMKQ~|#^(Jq_LUNAn zW)OW)tK$vlV6aq)wcIiHah{9pLEDZsoX2v~*_heZuYEBy!G2~7-+#$_+`aiDwnD87 z@*B?4V{U!!|E4T3j|J^Va*jI%v;myyr(Hd-DtO1oocCaY{KL z4Xz=wUB?h>I~hdnB0ga|vVB{Q$>HmzI0I6Q$P3dU`(VJAXNKcVcS0l%i6KRa=_Eg&&MT>gROgTZ*(& z7DI@hvniyl+3kAluLEKW^VM%*61gedw12gcO`-qcjUCagk zMKs@<4EPPxIO+G~D6WHgO%02kns3)Z zRIYZ|ch-ke@aE)Vsg!a7gwC*Sq9o-u*xnvR(?M=?dC!>v%@#7v7{^Zf?q?e+mq5-) z);hz=PEUEe*e}Q%7C@ZiQPGxT8Y~-u7}<{Vwi=LaN|+D02KyjPXV(SsP7_Vf{F1?3 z8=(X9GdF|-^J}rx{nbjZt#@ZERxy$`;lx8He7`?qhZ?(Rr84yA_=biB8#Bjokoj$(hM;0@uhyU@5ZYOxyS z&Hp$sn@dqCg}Xqnj#ne_4IKe)IBvzfA^mIY^ItMjktTbm1)YJcmp=+AUIv7}I$w>z z7a9RC+U#h09&`Pi?|yULobPti%k;Jxwu=RkA9$7<`l6Fn6An%H{NfW6VW;}@vg52H z8%>=0S?`UKX-D5Ep4X?H?}Sdfy;JMW8|GgYJqG(14KK28lVH+X^KwYJijT3*OWV zHB+MKE53}8zwd4DXggw^XbK9mDgyeIMMEZ$(0*C_|kaH(%f8|?2^rz-i4)y7Y z+U<2k^45X5&U!;LeYbxdw2$q)?NYMBx#|V{7w=5ZJg4TrJc4UinGT*LL@He`i;> zS+w6=?&y$3{QkJm4vXd`lVJ$amT1=~`lcNhRb0xcwv(IeYBOgTp4 z=rU~TESsOhu}131>-#zR?M)IH+jOU&jxl`CpK8qo9|mNe1J%6!7K;z_#BN&|)hQpR zZ3l*}V+vNxU>)GQrtm$reM86`yu~K)-fCC(7AxHy7P>PO-J+OVW9TMt2MzPLd1GkY zSr*-6<92s7sN+7Hxi@a?=>y&b{+Ny655D)lzWtqV>+ZvcTHd;)f%!e&yrp_@ATf`h z@aFG`P3Q@42J7BR1DnEeZ>g2R=5Sn$QW@m#Z~TKt8%R2!>p*|>$8)1rPDk`bgM({2 zn_d@RL4p^RdAWTmn(s;F9W%+lGhQ4QHjX}vfcgSKa-5R-!^#fJ*MqxzHt(0s6IrG6 z%yDIwFYI__mb1or&fg_}wu(P`$wPl;5Ku%a{Mq2eDJviuETy5!W4@@H^J(N`uAB3i zILX8VOa{+^P?htyxgAY|EesxD)zn&e9Oc6g6hqADFys4j20~=LLAT@$L58ax)*F^O zwO{8=jcz}~sVL~RD~)ZTQb{26~*@z_MC_;h^8Ar-@<6HQRqsPazvasz478u@OnkGD`G#*n8uz6-axTM$uzYdwF zKFj856i+_UCqq3*u;D8S8-URzM)WOAsYAduq&uDh1<2PNYCgeYqiARvc-tvSLL*Wa z`C$1s@g?=)Ax;c9d!87LuPn$zUTj%TI0YMA`#7f@15>O|1X3jDk5kDU-NS0Vn@+m@ z=ow5e0Ptr?1T=#V#8trj%WArxW^t&(k}6ZA z$pK&}Ho#nX7F-M0Lcp}B*JA2qMeQ&x=`B8$hkXk54yw$K4LP8eL+}MKXJRC$VSYeo zz1aAL7RE`iMy;e4QlLhxXybbvdi{bZptMc8O zd-@J|=Wti=-q>eTc%Zir_Vwoeu3qC!iZ_qNp}-d@`)I0jhjq_`AaXO4@cweVa}h*)k+?DkV_RnJm~Zjn6e^`Cb%@!I89nV| znfhvKi0eb#Io~3YOIc1~oGc^Z%O^6Q&!4j#SqHV64s*(PR7nK6<`A-hF8!5<d$Gih@o442Z`2M`d+r7Kx zM7MOL8+xV(2ZQeK7d?c&4&G$4y?L%a8N9oa{}OlO z-v9s*07*naRJiDx;*QI)PN+r0s9F{}j)k6XMh)8zDY%H&fG5&RKreX&7wIazWcn+^ z11r~FbD&b{VN6RYDrJHS1AdB98jlsr#h+)AIpG;Z<|B=+3l+W8QHviam&5t5s%;zx zU4?-edN~G_VvH)qFmJ#M3R>qpXy!~SOh@2RG*IAmkV8kT%v^%t0$v7j&w7(n9z0eK5SUb?`Xh+O#6|S zpx8*I%FS%4Hx_e*bF$E@Eb=y>43f(=2GZvJLPf#Cpi*!DBc#5zLF3h!=8<)TsAC|> z2nI?qtPj_S9Dp!Ni8Cn0Uaq02)lwwdFOX7P4OI~6wuode5?Cv=Va*$h$~exknRJ_a zEceq0`dr9E<6F1et0^VwJmz}gSMWDn zro6Euk626@{w8Pr!UiDPAK61cFh_>!`x%ue(y5^+e0>9o;%pzRE95RJ!zhg;0{;KrZBZBtprK?Q73eH~C1HSp zNmZ@@F3*i$*Vz!vnczAYqK^S+nU>mT#l8XAH|hx$6}Ly-h3||?eIAb>g09NGM#eGD za${T*I!i)dXTvNJBpvIZ#abFpLS-G2?{i{`iexwzb;jdwTDs1)ZjRXC9cxk6S}M5C zh3h=1Y!({o68)|Qv9R3?8f(!mwI5LTK3^nz%)#B06CKjOTa3Z&^@;AW8GMub>;2WB zpWfQj|MlHB^>5z1r$4)Qpda2`>An3?@9q!!)?U$DyV9FGMXxQY?$Q1oaF5A$ZxOn; z5(qumExL{Ua9C>(`|c>M7&E4i>d8jS7(mg2zAnLnK^(yVKLY!8c%F8?(cr!o{fkRc z7$aj~Y{nuP3)){Y-xi>t*Ze4_g4t4vN+}vZDd%}#$I2zHFhD-22|xS0{)GlD=;J9y z`a4X3=Xxx-9)@EE{0sdrAr&dnKbaP^6{=kS$~7}we`$uaY3i?^17iN!HlRz;*FlvQ zs@TS8L_dbq#!?b_3we92X~Dk`=e+XwmcRFSepQ}d6~6Ke4SecE13t*xQJGgp5L=jY zG@?T1+}DwDD#Y#bW5dpJp+y6e=Vn=rvi-)xT9-FcE_XfRIAL)TU*LUs~ zeedl9y>_SQ^}C~9zdh*n8`8twwH~gH_1fx0Z|sKdj%(dx&fa8`xY}Q6b^B0+TzN5Gv_innqCJje3o!q!57uF4A=d1EAmY-kbw@s zT08@lpelR*HbA?gVQxB|f%WHcMf+Enbd@cexXR%R*l?i-?nT2)&h&rCAMW@ca{sYu!pmUpcST=(8EA~#! z8^PyUu@S&_J;5A3d=#_2Da@HJ!dJfgYsIe%=BPG_KgxSEU6 z<5^bgBHlXZ%oe8K?}+mK`OFb0s!n*~;R&B)I7` zaKEkC{N+Cs!-j8Ai!c z-rXDZ_Kx)WGIW2~=pGw9|DznYwbq@X>Mk(7O^5C*LiZK|MYs6z?IyT^{RR^;|Ht6; zWtRaX6^nn-?*{jm@LW_Ed@e+Y@4*=qXXTjLUdTs_b&d|tp^5kWH$2X!&x5uk#y@16 z{kAouN`9i*FZf1_27W~_?>?V_L_X!=cuH~VtkcS}wOVQ@jie=c+A%E(bQ9=TnK1bK z#~^vD**?Nod06L0O`XU5#o)_^9M9R@p_#traB8=676UcSG2%Az{kh-=tHtrLZZId` zJlNA4yq&(woVx2D7Zt%m!jhj2j;1Doh_xa^vcX!YM@^Wt>|1rIJ{AAV2wKX^}v@4T(JeOvYLP~r`s*z}1v+{5llBmA&hY6J`39FB{Xmdjm@ zY*IBWnDs@^xKDre=!t&y_((s0e5`-|{Md81(1n));jhj=$`SCa@h>HXTd@>A&%w4o z%7J;+=~pB05+mRR(|I^u*UkB^>+~v{eJ-+}*L@ke^6+!sj$GXGpgFJK+FNqmSQhgz zJ@1Esv)R1JcN_}#F*PA_8^PwwcH8XZJVuv;y3AYW&6)kp82^)Z_|{OsEi)K$Hi^TsZ18(H4KN7~jYS9~gqhSpZ|=`~qz zK5f+F`Z`>1!g|I}NvrH@$m7FesnWi>4LRV0_5&VgPWRVw6Ga~^URn&C<1;^e#)Sge zXqPY`){%o)29G7w#k_4sOn#?4DTpF5Oz;$ea0cxZXg@6{Kk0M5=C}me(5D@_`G&U1 zbpF7WsyQ`(Jp_F+sDf_H`akJyGcL1N#`!8Z_9|porjOW;1Fg;Y$^1N_%N}RrdE@h% z)BL{f1T)_44J)#F&mY2^uh4%l$Pqd1>zWuJ{OsL-R4N0ztf9zq)kaa);6$Yyu`zt6 zF`jA{9O#)2H;;Aj>@yua{#du3e5QNPKG(O{ti8*o^8MX~esFV7KfZTcKV~ENqx-k@ zgL}91*4|QY>@M{h_6N&ReWU09r`z4J=+2_(HuN@c+wLx~FBkX#sQ=jLok?$cn_6$N z#<{`R9&p`uxu*vDNe6#F&Nj>{vug5Qne;Ptvgvn$%`unS9CxIQcBW+9+Xm4%Bk#4vG{%z^I+h| z+6D7*$#!c|kF;Dr(*X`|F%KWyzM(zl;SGMMxb@n7?cKYr!-w~Edtq+t;L(KX(Y$$#-SGb`_gI& zMu{Ipv@>d04JxDLx5=wAXh#d}mL2Vm``lOLTrc(LsIUoqqJMh!Ouslf(yw^d4N{S= z4ZYOyD)^f}0;QC8vp`EJDy1QBB*BB^XL`#WdUb3^Aisu+^6NACtpx@>pMQAg*W52J za$W{6b3xp7qhIh|N6*>iPOT3X{FakvVPbxc+(=xY?>-mWS=fk}j`)Id&lx-nD+{E` zgE6SDw;}XF-czt3B=@Nt>U_r723Q@cZCm+hw2{FK9T zP;<+%QMDeseKu^=oF{#zK2&_u2fsjPIi|jC_4ou3cGJLDj7N}H@D*E7qao{4S7qMV z>YW!`0P8iEC*C?`JB%s$-G<{^yBo$2xV?mZ06e=!<#jKd#~=9r(@&QPrQbl;byPTh0N`HOTH*}LC$sPh?)H8tkE<>xVt$3z&m ztxI31XAZ`w8&Xc8jW@GO)%G3??^pwa>~ApAL=odnqm7`NN894D@rJOlm>%MphRqX= z>qj~$&vbKmrrUa|JN2>d9Dk;JC!g!#`mx@`@5i@x^^>~?`pNy9`qS6$=*JIl>OJ1N zysK3cv+rXQ|JKiJ?MW=K~ z`%Qcgse8Y!wF1L5UvM47(oW+1o3h&dr=^UxU&$_~VNSfnhrD2U|NqN}ckbUsa?*{W&n;wSK`VK_moof!E zQy%lURLnk{BilmPc$n?!d=?^el~Nc7X+SEas1(bmbhM>P;ya4p7PKl=J37{G9BDs} zb$55v{TqAQ=PjS#^xa^icazQB-otxZ-oLBW-Md?%mgd&EfLlLk*y?VXXJ= zY42Na>+bhH&^@sK)|=9;oAR5$x>~91?kN^*K})Ct@pd~gR*Kq4C&wDt=q=acej1QR zoje=#`R7%?{`6RX|Iugqr$O`am0hTSLdq{_zFj$%yP|mB_9}PNFAy_)-xD@ zJ{)a;PDjG}l;%FE_`~^dAHS*S*Ne-O>~*N0d)qh$!r$p)F`USUBi9IcKcQy zM1~h#b67XW+-BakAT~jM>(0YI#(900t2Z}n7%C78_JCIBbiRqVt?78;SU(|k_RSoX z@X1c!!umPv1KeJZGd1knsre4;3~yvi=djMOE~s-G!-lBxCa+jab`7_=;b=mHA1HPj3HMdWv9s_2kJU&664VhB6si5f-qEDLUu9D9j z>PMt5CE`Wj3pa)To{p*IIUJk%4Co&r>uE$zw&lsA?GY2#=_Gyy#O+;#p86u2b+*?W z->S=~eZB~KKKssj%U8qW5qTXwT)t+8V|;B5^9-%n~7WJ7{xruwEr$7H&)erwsdyjslhxL)(KXv)D zw*UYT07*naRFMAS!J+;y-+4{{u44oBVwdUJQAeLW@5 zBk)+O`h+(&kF|vEurb`zvG#czxfdte^Wt5{+81CEe^5_!sErO|qgxEXEjEkZ2pR|I z9ka7J))M$Wr8Cz)bBu_-4kfIe(TH<2?(FAzeachJ+>@^6H5NAe7<1ls`&-{(+3U`FA zG`Y3cbgtd~PKs)99j?@7+`IhOoT;y2=}G=-!Whr29Yqkq&?SLoL4h9qF|P zsyB%_I8+c@mpiI#2I;xrUtA`FM=197wNn1Lc14tz?%y}JHC)i|F^llIYG`gW*Z2K6N^9{$Nb367mE!0CpxNTIh8_r=nO3sb= zNZZ_BLOr3>SlmGwzRk~FqM~H}oAksYqq(M5QMoJ9^(k%^=N=poo?ha|`PU-Gu zFlqy~-`?MIKIi=YgZu2)%spb zxv#A|Q@PBq+@lOG$bfbj>(tBEf^Xt$*3&ue{dq~i@?On|s6KPtC26Ih)CoBEd7*w9 z&wG<^zp~hD^*vg4!}+M5pCK|sq~f)6doysKyKUC@x3v$Fygyr5D%j}l+%r4I>V= za%kwG>s`h8r&)Lk1=DWXs&>}{aCwyKiY4`8R)!t9E%V=jjbWj!G9r`D{RJ{?tmkcd zpLpY~yyNd$8Zs^)q!Oi0at**g-_+pjs{h-NO%|&9i2icl2%p>e9Ue19h_Q1r=0w%I zZ(pRTWC)T{7P-n?Qtpx`@ho})w5ARjz4*vS*{Do*1aIRi#zd9{4&^+|FEpkFR4qkqBL{jtTv1r#{T?SGx- ze;qY`g*eYH9NGON8h`CLMTRkh7c;ASZ)A1Pee5XqF5y-V2Gn*79eAxGYCg?{$BWNc zU(ABYY0f?dn8KVaQ}(mHy1y1mLq9bC>*^3iJIQ6Q$!GqtraCva7QLzVWzbu|1QUxg zZd!#jr*FRLZ`Zq*wg%!`IyhLWHrD`Cy8t9x8RgzG!4);?t{9@gFdzB@r9VF#B!d_} zD^dWf5`?4*jd;8;ClQHT%6Qt!v`oi13?T%3T34$U5z*b2vrCw96D~khoM(Oj9)Zan zQfX(su)b(tP^Wc^I~aoY3*8D9ShqjbI@rbi`SPP)gQ?L1n;uqclr8;EeYgSdDo=$o zl#jUQZy~*;+j{uYa3g&y(@iU|xew_|)0L-SpiQppNzV^%f=4O~?uy{q- ze({M%IMCv}@Ppo~xg>G##)N|FKnaYg1-DQEeQEoL`z@XuAeIkvT(h#M5F0maCys3+ zTwq4p+x0{@-iO9xNuk9ggczh4ef)kPWV~W{y_^-ubKh~!FFv-S65M5~3IDOUJ>j}Nm3jw@N~-&45}3(jSr$f! z1Ae4up@XU~lbs}uI{Bwx7qn*;-51#2oZxa9JMQL!5@~TUVr;kULl7^083Vjo;4uuj zKsJqFE`HLKl^EeGthP`xZxrmR;aY{*;d6i9OK9jf!VurO45x^}?qAZ0USE(i%@YLW z5_IvS0LOlt)&loq3zO+*`=eo!KN~xi05+|OKLPoV4G3EU^~{*{!!>w_X<5os$6LVo zWS%a*)`_R|WtZ!#s^#<447_omyDlHvHGi!O^sCKXuHG(H67Umpk2CwGn-72PK41#x z`sUWA4$B+!@5u7f8}r++Y%x9yYrV&ZU-L;E^tkwm-;!);V%2mksmw-U$Wb$L$2zFKl5)8VSEe>-7EW-_oky)yD@NJEc(q~KSaT+ z{<}ZD*06(cDnq%s*OEJ9IH#5h4?P~{nP$yeaQa!AWHEs?Gvjc#T~#qo3`_^ePIR4T zLJj-C&#g7=%XGVX-OX~GBAORDG}Df41+<*r1lB5y=(}bXbX+z3Zmh)MxoqX3yfa_7 z>Wx)g1CAahQkK@tWt=%F8^8YRLuV65&5Y8`((K;x+mI$@aTBSYq${qLLg*mB!lVQ$G;CT72S@0 zX_iaxf|l$M%=XIYrzaVqE&}`cWOMTg|J!#`3SFbKQwGzvWJu%EBm2i@Y7ww!g%sNhPcf|=S2krGwbP&l3e#2Nn!RUaRgiC#zl7!`<_ZOjTHGDFm5G zEERXy#S3|n&si;gMpDTSzxC~TY9YV2H@)mW6i#pbE%Vb|>XkTa+UJS;%-RJmHM_%e zTL7(HTv>OGi>C@MadNPTy5f6vQ2WO2=c;}WWEUI#DW|KGy_&g26Fj~#V9j6I+eyz2 z{lV=_-R(ww2i8Cc!FSt=<-osfiaa~V8MQ> z_+_brmPZFaN&jI!X~2&*Vv~|{$#15hz@yF5eN^W%&3_4jJuhxjUq7#0C)|WvPC{8D zoS>|zw4?905R5;CdwlwS&YPDOb-M5>irtl_Pf%e|<=~JV`W-SW|KDNHe~`ApiLp|8 zC+eERh{AqJr@BESz9Ps^G-G@pl@C)WvMHK$sZrqdC8OT@AbHLMnV&TaVAf~D>4*g+ z$IRqF;lLb#25<_s z)^HdCcQrcSK_-@CublH@*+dgu{QL9+#JFJ?tM&l`m^}^_EEyoU$67npnrxA~nZpP3 ze?5$Aqm7QV_OnFF5zELhoDakL8ooal;1FZZqwpZwQifQHxROfBaodcOb^{ zhVnQHPE#!UObwZx$0XDNcj{HoXEX+mdV`DCi5v)DQ1MSnXE@A?0d4xdR>CH|ZbrX` zUtI{RT?sdQYIXQ=0@2owrn-+-dLXzzwh!qy{~JaPdOIDfv|?ylxqYD@iIkOGrs{5W z+m&d<1xC+orSPaPl><_nGBJp(25b%o>eJwZuyayZlB6#j?9l^06V?3T{#f8}P5bxZ z4KE>>-=2TMH>>N`L)*7P`o?Ab|I2FsPSl04a~E895aR^*6Xe3`f3Iz>MuhYe^Ji*8 z7&^YWYgb4CpfR-N*8A~mfj^hndywVp1F5k_Q8;r>=f?`Ah4KbK@3NMgjZ3lmI=M|&^Cb5Bzk&2b zjcUF_&qTpt4T_$xIRfOcRMAhJasXKgGhD~KoD$-hLA$kp05?mlrdMGsLIC|srr2Cf zLR2AJ{((|+>b3Kq=<-Qkqcm=(lRl{9I0gHbc9dC{cwNM7WYqzW<+*BZaz1AoS}L1 zFV*N%|JDqS@uR;^xytV!CyF-mkp|1rGWI_+wnWRn={KiFn3+zP)A2tSCJu!Pmh|-& zwf)m%UgO$1(4seO34n)@LKyaU?QgD5p(uq$PqCt4bi++tDLNzcgt)NVrI;@+FP|2Cb5+|P0+0S zO+DlS`V2i09&kg}N`J4e2K*v(i_oH{&)mPwI}EvVkm-mPjVAm+Bs*l*{_bDt))7Ns z_P3*NRVfk@_Wbpk_lfVlj(#MBJE|RZaDWp~f$#Tlhr1IcWIA@Vx;(l+c1(u&`~5!Z z$o7=HFF&2R&rda;StnX-8ks{xM;#kO>_+_lfEUk`=4;)7`bGpV0-nLt zj@(rXB$Xo>l_Ea;=+Jyu95@Jp(rNa{3y@jBLmD!Mp{XnY! zX#ri@bmS)jsee|%{Q}?kEaHrK-_ZHGk4!|-dUKM_Bl2+f_PzH~;I6`?&@YwP)bTSd z^ta+-YqlJSDsj9L@rSKQmr(~$tj9{lzf^c~Ota(I(zTVdAW+O{msy;_nqRd^0XoOi z--ytsv{<&k&=-nol4Bq@D}hZd9F?9-E>(M20cpoX;ypfGe{t%)TEl6yfiVHA7P>Z(CJVuP8x7VK1!h+gWD_pIljRe+Vft$E8*XDKGahXD*f9`wfxTbwh*!Z?Ei0{8)~$SO;MoI9dQbO~kQ;ghVv8P~W7%zRq&u{PKu6cI%5;zFFz)!uI>I-t3&K- z>8ui0#0nW)>^fPN$$f#nC(fgxSSG7Bvg0EqDH39VWiR~^?9u2fg~xeei(^FtaR=oS zeufJOWVJx5+e8bWnSK)ohY4gNua_^VM*vloV{04uU@B4H=3mfmD_4X#1i^brJ$-6{ zdeu5`N`03)=4^7KGn6E|(pqjh$=b(y1-1zR8z&%S84MFf_0q^(DcWE2AM_ zo9M9spK8h?h3e`2)t_(qvRm93mW#GYT^h1j_} z-Im4TiHRqbpl@zLMeVXz|L_TUA=TrKnsTwCq|#pffj=Luax@XY{`OJ%j=-6q4uyyp zmua>A)3}Pf;Sq6)kd6<6Csl3dGg1rzS+Cx>e-e{STFkS@ z1;Cgf8&5dq8@v6j5vx$SZi`v{QLj>y*J6aqDb1UFB@rOI}X6TdpM2kH>`br|>)+5Akl!Fu7C6kytB* zuS-BNUg;~eeTAw$}E5U*)$=kM%Pw6jpn7v?aCnE$22Lzhp#u6!&)Fxlhho6kQRV zmny|Fqp{@xJ1lRz3iO1bzGB;1K@7fmY4UNsoZsmR{WjxAniBw^q7z`N2NrfGxmoTLK2n#+O2%RhyJdv!8R)y{O8;>v-^YLLkPe+P=SN`EWYS!sZ)k97-3iY~B&U zb^Eh}aL;ALulD+2tHytE*7=%AP7XV?QSED-zzJ-#6rPziC&60# zo@nu~2&C@r02kzl0v@S!6Zsyx8p@fL?sM>GlK`?tT@ypivnxLg>ScK}NKGz$(WXpM zoLp#RcAMu+7ZN=iRlqn9>uP_L;6_cq6kOiRt5f!iRi40`b7iL%0+s$1UqlLL8U6k8 zV)(N3kw#1L4^%^-t_%e5t6c{t{W&;0(kV=1ozO@&aGxNdGWjE^z=({_xwv1BFJpwq*Hm~Tv-1Ph?e&w$8O$HZYJL7xa*un; z*z(MuO5+aDkVab}vHv^gKzQ6x@QusQcjMf@!y7~AcI^#@+;Pyr z@`F^F-DdVyc1uv`i+3p`x-fG{CK^;n0r-ox=-)(lH9ID~wg@^n=}uxeec?sk)Z#S+ zNRj7o&eMz)s50d`o|sQy@55-Hru1?+FMey|j#X>GO&>)$=RC6`c6~xd*_PD$tye7b z^{1Lqw$K-sYw*hV;mQiXaZzu39(~=AQe|ed!~1kO)%={a1}qxxX1%^2Uu%S`F|6|+ z#W&Y1O!JqK(xhy-@*%Q09O2mV{mgiP=hY9t705W_5f~yGoiQtOQMX!S8>fiH=(zMP z>Ev3Ov9Gh$&DtS(H{!*ve{uH9ez6p1I|ynv^Y1g&GwM59k#_AJQ;s27yoA-Rr(oFz|R;R8Lhc;)oq1t_4o%ZD6=gdmju= z2=cXcO%)aERHC0_iAFyEkRi1e-Ix?i9EaLb^b3@} zB=w_!tC5YID)3=?!0_)>ctpU{!n;e=-Y^`Jl=#q~v22HTC888WQShqr8Up85DqSim zAa%w|e-b**DGT(V1r_S9-1o4?cQvad^)%5k z$eBcoi)yHu7ipa0T)JbI{zB#Ou5pXAM)b)n#NL`SxLRwruZGqR*ilth=no>|euBOB zjWN9_+yD0ghb3Rm*C^aM;I>hyILRzg`h*bZ$%`>%G|peI7%xXl$YR6wV6wxwIiXis z={`Ta8fbyY7rokc5tD8h^{t{w+{y`Dd7!`jdRhf0UZARPUtnBt{$-P@G%w3&h}N=} zu&@XAlWRq3#L6G`1cENqL2oC+HiU1wT92OAyY*~OTUr4f$b>A-K8ZovoJ|@kn2LTo zTSCu`Jt*9cW%qfg7RrvUxuy3|F75qeZZ7Rhs^vl>Lhl*c@fo&?$5o@*b@$|+_U--+D^GmIohx4xv)mt*P_LUlL83?|f8eG&OZFLSjb?T+ zs?Y%Jxm1l%(O}%_3Q8f#>5I9~{TQXuZJxvXe1DXN=;Rw^kqWtNFdA^rPC>|ryWc52 z&&C;&Pa?P8d-W(Pv$QgDz)!j@shpdg0uix2YD~t^IP#(gL>?MrCL~?!|&Nt)x%czv+7SS=pVB7Z#hTXUP6mjeD@)>n&%A*=G=e z;KppAbCcF_Gsg2OKCVyLkcB4A zQzGtrda-c$_>M@xs$j~>7sEup-vf76JEl1dtEnH})?Hu|UQq7>T6s#uGsY}_lR$_# zh-8`35v*jD(Sf+lWomW|uOaBjMzG~bp38`C0VRBa+jAOw>lClu^Z0M>cLIt<^%cfO zSuVipn*tTU7}b8y6~5)9$Bo|DnPr&U{E0=Lr0D2Va!5KNTf*XKYdQN!C8l|$x`ym( z{)_%9Qe6u>o(DoVP79(dck0ycXTvHHA3#sW zuCn0*5s@DB$)V!mZfOZsmrS3wZL|G{X=FCH0d6WXmJfsB`ZMftopTC-aq!k1s=)Vd zP)xPx=_{X`mT1wws+F1dn9IT{uwCleevPesV`%@e_JYV{&d2vaUGL>j2zTf}wXO9B zXTst3l%}RY2JWOy1)kNM$1C{BO^j7JEwu&*DD#QnP}guF&Tufk$IuVTZa&H}PR$g+ zzYTpiqhJoM??b{x#J_Q4W`qDWDUyKcr(R;kn4`W6zCZ4z9kwxvc&D z$ou^?poDjcS{(H78KB!boq3nYf7^%$?a`I3*DvIao%&7$R3Vc#-T`lK8H2|dco=7l zU-iEV7hA*|y?=W@E8j_GDrEkDaiU7j8Irs4VlNEmxSz%tsOG>ow%_(44Q2)2^8+ar z#8Lg5xx)|}M*v-$lLI6#&ogmbPYS;d{7FUHwAV=0mMK~X3={NQG>$qk{JvIYy}lgQ zPxhTK%E(T!4{8w9k4ADC3;|Sby=&zhJCamPmtdv#y_GdI%GoXwLAZcqr%#Lc>r4M; zUNYGeGQe&#&f-o88fA@yQ`n!PCV3Wo-t`yVH5nh))6L9+%O&>S2etlHi+$`OG7;`Z zUgN%~3VNfw7@zP7SOjlXNWleyUEb*<$M;?U>QV@!C*8LDgKI|v>zEOBaXSs3GBPT_ z@%vtzZ0|`g_bx@jz0@EBlwwoR{CHK=|F=`Pg0#BZ3tUDp-~PLdIb#<24$|7LXuhq# zbg!HlJ>I3@#R^2=WghV(aKj30Z(;_-OfA^UScnd>R^yi!R{pJI=ATyJt{709aF--^ zNA?uFmn+h%6WcbL?2#@ps(PtrW*d6<)Fa3dQLmw=t+>UYF; z^JmYca^9adOrPU;vjah?+*pF%y33C1wLk7FrD`7=*A|z`$6(|OdHj}XXJ0{nogL64zs@HZ(#O1N2V@ynG`81~zB3BOS`^;yr!a7}sD#PN zy>$B3UHiA@p#DR9ibU;0iqi8rj!Z;1w}7+RFqJ*GtDmjatW+ucM7>F%G?eY^+-y+rhq zaZ^%vZT@v7<#wEkA)8Y_C;28U#-}wTNJDHxYLH{~OdnkTn7{qg%^NworYD4U6WGJ% zT?sFIh{$lx)-6cmi{2LNB`mvN`@N(7Om~>3&i`%ayioV~wF_#q$PBt}B8N)W*VuMPOZn&qV zTCMmCA&VP}0C|eb-|)=#fz^lI83}u~cagh8pJADc$9C=>Vc4_m@`VTw6QglXl0X0Q zp<8KhfAS`ayDMhHKR25spE!In)?N$MXDYflH^Ftb_#jO z(^q^a?Oc6TM<-+yZXqk7fwP)O5)~@9nwQ9%%gPLA7GxTi8Um|&Z*aaQwra?<-hrkvNI`F0rZtmSW1%cM#l*x>qt?`(QdD3Pf z-6!j*=#M+%i3K5t-9dYi*T&d0dRZYB9(e0o>aE+E_6N-C!G&+8Pw=b(Sy`CM^PDHM_PpQF`-!?+yMIKtACVPCv^ln!<5UJC=iF=Rm*LJ19{Ao{Pc$@Ou^kvI~=%ugE zo>kkr>FRe8ANC;7gy+~fz^i4BX(YVaU zhO!b51(+D@`v9}~N0|(ewtq6~%R~d%r=w`Lkl<==|4 znOBCg(G9GR*ubVTzBcce&Nr$HPtYph?E}$}c+{pl4f*7kK-yib=Ah0~FS)8{u+UP9 z`Owk*m__PY(Tqh1yIaT|0rbTxa^Sp@@Hlt1=3fg6ckgYT&a!;Pu&KHp)7-Z)Hcq<3 z!5=}Oh_^(+L)>kFj#wNHO_`l~W0>X(3WLFC+aShAjShAI7p zk4(FlO$C{14P!aP&>IQQ85gbi@l%|nlg@{``=gN}N zIv=hg)+|r4SFgfMJUB2z>PM;)!N$MrD6-w)_LqNMa6DJvG-Y)O5FytV@fRb&jg250 zpKc(%3uv6+_m?bnrx*q4f3DIv4X@R7_nmNcWRt?4XfLOe5hJ-k)O;4SGG^o!G)YFY zfmP8!iwvUnpmL+4(x$`gw)nMNxAltFFY^!RGy9r}P`R(1@~hER#pHAQAvg~E`eLOe z-Nu5Ur7@1n`bt~Bmd?`-cd0X;QW?~25K`8P{-~o7n887Y61q`o%Wj9C)6pH=NBA?L zi0;jcaUhoKbuY&UwryUL-K!6D-~D?2_Tuqz7bWGddXj$o83&) zWb(@uCww@N=K+(x#(qXnAH$eO$!Na+$!pQ(#G~jh6D%G^xh1F2=W&wL;f$Dc@$|_~ z7DL(fl~aAG*sw+={pnF#IN9YEq)jyuH9$806NruJu2Lal?A#UcmEf{?z=cW)Rp2=r zW}SKJcj>1YgHm6xvd0+gk5OTBrJTD z-8%btM9U=-(!fJt<@ZDqIt##{tuiZu*^6Z!;spiXT+wXeMS(K{_h^B=`7UpPrG#7n83;eWS8m9 ztuj4}_v#z3CqREXfvi@U%$Ti_LLP;5m_bGOciz_NQJ)2I_O(tVXUiONz=`v?8s?_j z!3wENi;k6{w?Q}2yaN%qEW!=#meFK1UTXwx#OPn^lTUlGgZ6LZ*7vk0Q)0x7>3 z4-#=)ZoE(8unByQ;k4#5UU$v476jY9umwcWZ^&(CAMfMtxpO+7#&)f$sUap0saJUF z#q%@eSpk0jbw3aSf0Q!Q62JvP{>}>Sand4T*}v!$91BD+fv}eonZZ$oh1AeSRd2b7 z0{U)oI%35yGY8FHoHq4r+C7+ta&Y`<$Ta6tyvh>9cbm;L!$dbMQI?CP%TVzlmx zKczM9w9i*i(K$-Cgn(3%SV@>P24x{3}>9PIa5Weac z1H6*KE(;h^i65=F+$^B10GzC!2$;q6Xz@?5W`5nPtRxR8u{P`dVCcUZJBkut9$RE> zb08SjiPdwI3;K1h&EtT7xxzq0e!#e0(W>a2u~riCD|7{4ki_k88uhWUWT6Y=(caey z6wO3MS+MR0fe65Yd~s!b*kPmC{&7mS$&rzV>2O04Uxsxq#jo~#QIbPI!B;sS_6R0zFQTm*uzCZJo^yh$U1@Z8#Re80#*RK; zR7E^p|Fx(#&2zp$>N05ESFqr=A$k6W2a)c95q!elgN2UYn;p%a@K0?ToN9g-K6m6L zF7)bD)l!o3k4%6WMNe0(lX$>FZ9`Vjv%H!4b&dR*f0^Oeir4>-%$Po2WRlCr(}k$eUwhl55~Udp>)%hui}lXQWJ z?dj_`Vm2amTZfGf4!(5lLPAKreT_bY8ILAwnduQ?LCgHg>v|rV7fFL)AvTnBhqRO} zv~2Kqde7@(<=y{>g{OmqsbbW6XJ80aWnI-O&&k}ro z1UfvIdd$hI>mAqF7xQQaW z5|X^jM=gy5XCwpen}I;lS)QADPp51^D}nD_@XeJqdKYxGhPnbBaia=5W}&F7fLZ9n zI6BJz7K(;c3WgXp{1?!`P>;Dqo}dge3)k;l-zE>*WHJqBA2r7AgO#C*B73a=^pqB* z^Yi)FmiX2t%fta*Pg5I%mKYrVrv=10Al4%YdDyZ@KSSonFJIU=9q>MPX5vm>>U$Ye z(>9FhO|fv>c{Npd1Sl_QRIp z;DbDC843bRt&6%0t~c@YeDW;)eMF_7A~8)ZC*F5@#{v%9eaXCJ2YCZLInrX9@{Tt# z=3WJmm*R;Br4IPHGVui%**{a@kJo5I=5?^ITMWJ3suSsx*Sld$U$eGp47HjizykKA zO?uh_525YMW3bz&tih4bXRLtnF+lW)A+)nlIJ~$kMjN)aIKi^!rg(SdCc*zvWtZQ6 zJ%QJ?Wx@r}pxPctw=k!@`)cY>Vin6yw}6a6$N8Ra4&5U3-%gW*m~ic??u6j6>n1eA zwn`f=j05B*_$vBah^HXTtNY&S<#GPfJFt32KU;pBrUrXq{CD5PDVsfIk)-mL7YxUI zeV^lY*~|JM6}iC~)WTYl#?xF7nmiS7=N2KW*L~_|#d7XT3)4Z7i)3@2iaLW+JJT&| zKlWgiUhBSEDbD#2;KXOhd$*$S&zlrJ%CQ3c?-_TR%@q!J6&TRw^TXd*rTv-ponP8k zk8rfx6(`jE-l{3xat6gM(D^qo=xbMAVc;uJuylW&r?fd*0IzMGE!4y~keD^)gJO=~ z>wr&loW8kxaD>a0m^+bcrpGt0#-ysvS5Fh{isU&g@D;L-(i`{~gvA0BYO-eV9@Lej)@Wgg#uPAn*4&rYg}0 zDr9)a-5C;%LZda%CyW8*P`^L3{rn)um6>vn)k!hQhe&@I1eJu^mPGkHM0KE`muF}w zn(UzveSuz#;A9>hQmn=~SU{JW86|+T4 zRT{3K2E+|~D5%6BGQm1BE*1O1J;T%HItH}eSEQ66G2qalH6)xDD|nnRU)1ZMU9EHR zrb_2R2VG6;dVp0w&JK#?p^WVGbZOBZ`o0fH%O5%WHY3+eL-;ZyF(Gz2m*Hr<8`&urL?bGIz16-f-ki?f1_rL4L^Ca`v^v+tTYmfM(avOl|8;DIOGE1?>}JlRs9G<0=&Nt* z!6c~&GcA7JQ9MC|vdo=k@pUZ)?d@(T`NVMZ2UAFMF!ggM(2L#Rz4+oY@&$r5%I?(3 z3SK<%lftaKk4!q3tm_Iy5>ZJEwM@R)!0A|^=ntT8!z3%KPJg$wTfJu0tz?dbbR(CF zjz9b7>5ciwQO`M7law03ajg2$*Rl!LxC-cO`WZ5hPjX4inpFs&v(R%lI6F}_82O4N zVB51hRT@)m{-Yo0l{jYGg6aBk6aF#0X$w^D5PPpxJ1{X|L{rK?`!hZFyHRs{`XAjD zI%U-Ftnn2+4Y5eq#rVgr}-#>tKOEF}l}CLXOE5k@ha^OgZ>p!1AIVf9 zTzz?mUnGcI=_w%YBhg}C0mS5Ebj*n&iZ*>-mX8X=Np(4 zE=j}OGiNQXInK!xCH@>}1uH1ASA|WGWq*^>{1K-~>wv)~TjwRmVe*(Ba?^RrPAf@i zT8TgB?gwI33E@=-!@llo0tf1MLqi18nz9fd_Jf$Pw<_o+2e+CEQPI`?TbuGx0wTvX zwBXZe=(AWiQYr=g-La1T2(_y#yvIF$9L|iiuu!i}N#q_*``UH%3qH>uWOC0*z=UFi z!=UpwU%Ow6|GdOr+Dn&$&Pa8Lup1I&l$_wDRY6T5ddSazWlVrre^K zwD!*PqKMdY#UEcjJl1u$^Cv<&t~OwsS-vzWta1zz217Ny*(Pm{-ZI|)qF$M6UWs+1 zFYXrddt-IDu3FBW&^A0aiKv741sgo7@%ysOCScmqriXbq|YSSQeS^IcqN_YeyTmA==JtoLxZ7fWI}vj`2dl)ts$n zt5557nap-#vUyR(t{3_Ud09zx1j#bD>*dDy|uc2dN z1FqFdI*_51P~VIIYp0t*W-dm*it}2v{Bhtk?3O?T(HrC?R^hlmd$cy&B8f;r!qJy= zNDuT5I==%sj+PBTNuugkA3V?|LjK4?13y^%&=N%X=G3mOqi}xV-b9TE9Pjc!xV-O=Tz zkp6bw@v*pXf;?o>H=7UA(}PDw+D#@nJ%1>(3!n?cYvzoSuUVn}1?~S>*AaRQeO&Wy z%iz)E;%?Xv{fut2A_nE}D$I4;EC?yp^p z`Dqrulpj_}#2aG?Xb=P1y3Aj1`AS^#1T{1WnwR}&Nt9sUO7OBxO=!pv{3*IBi0Lff z`K42#5zk7Ec8YCv{@XQ&Z8lA_ovDw3Xtc!FD5~qO_?)xsH_vN&bJ9c!`TU}nwZc@i zs5E@y`h`JAi-z|Pel6VX5_vY7<^rU2S^czN5Nqg^FOy{dRWP%%p^bLu`X3L`X<@&; z#QBY4)ZgqwYfTJGH(M1yKCH`o6N*wlPO23N7 zs1p!+4yx_)!ObH64b+;1L?GgY0y|8*kwk)J==)p!j@;RCnBUMXW3`t!gLKm9dS^jA zt_1O1n0!8-PvS_*!XpD~eaHv`>~ku1e!cL-c(3t%A6pX|gCo&TYwB;V%y?9lCS-s4 z2M+9?g*U&sbJ(!|<&;P-Ygj=4i_~(_#rH2k#-)hLX|a%6#-KnaiwwK8h>|2Q(>36AM9vWW zYpuxC?lwDo9H6w`A#{ro8E;e2W!7L^HBT98ME^(0#O#d#;oU9JTYvaQ2&ne_?xR>7 zqwU#(7EDD<9%JkGD>)z3A{oyrhFN?cD#G{>B|LAi{Lqys2|y=W7QGpOszjcH&}&zV z9Y_z++exSb87SqNjH1-f;hGeaHda4gF@+m6+0zCgB(~Szy80>Ym%{(yW4aA0t+9QN zOd5AmA~Wl~Y-x#6v1hP36(&d09=<|{>snqfKKkvWV?D){PvP(|M$^Gt8S&0U`5OXC zlD0nnm)Jz9D0=Uq8+^{C#V7-$R=NxJQ0%+H{ukEmOJb_@MBtjY*+1OQwVC^Xb+4!E zto^~S$?ODvwH>|k!|E7`iYv~Jr9z1P0?xk_r8%du5OESHD|J{lBCpQEqKT_~W0_U= z3@dY~EjwWe7e4>ubv5;92j+g1ka zzSjF)#f`U4DzetVtL-^GU+jASUUpD?1xAM?B&wY)*UD^=*QhqeiFv$m;j)0F5&x`d z**Q(}tA^Krt~jirq5j{nJR^BH&Hhxt=2EKa6G5os*O$u9ZP_(XKy$gwf+6FxI%=L` zo;^kqxR?tb{t-&GCVrKnSj2>k$+cq z1=J1rx#Xk=RH>-moCr?iKErb=q(jV`B)Etkkt)_n%teewe&- zUCK0A+pruYw`QD6 zuzaD0U!;;O4f0jL1$$oWC&jGQg3!`OaRDrTs7O*l*2T55`bOZazr^I~Zj;8g1Xzr< zf6>rlFMJ9|d!+v_8T+pBk++DQUsL6dedQ5m(v>x$FF*(N9k*|}*6x7mosD@*H~9dM z{NSajQ<9{WblPMg?fTzaY#iV>c&DmZiVHE{5r6RC!64)(cE%{g5_qY3rKVT|vB^6E zXn1Jvc5}ZpP~MnQ+y66c6W11PLOFMp((C4j@eU<_HLzzNho|%!%Y&4LC5eMYm2CfL zzAMNgqX*Tg%@v>2qy}wpMBKqSE*H6f=dES61-PtLv_yPqS+Ze2BHkzdMc;69hTL{o znuSQEb$fN+yaN4%8@${6j}ap^%~R>7>7W?M=S{yrDFC+6=hU~>pJPyx0xaf^(@$#> zB4aT}sz>aK&n}kUb=QkQe~3RU9d49QAH%u@?fLlvZI!?5QSomxdscas1>tD1#Cc7I zkBp9A0T|>+01IEho-pmW6H7SbMqO)lEaNs{EV*5-wzSV@ z9T|nJw*0kOb+6AgXn#Hrm%}EZBOv3aJ!$c^(#darGj`uj?|d55nv|xK^e@H{pFZNu zv11(gW@K<*j{K}LtiG)--%qMy=i9q2u{|8Aj_AJN$7h0x!Am>>nM&6I>_@flEE6-f zgVpFy-nwk`W3|CvsaFx$wfU)Z|FLl2%cj2!1U_u&NSSmPVPC<_nD%;V$t&DGbkE_U zp_JQ0oi>t~O8JWJ{ed0l7tMrQmQ9%~Zrq3=P3qwca(LYtH4j6DMBrG>#TTztPEeD>d;&%3;kI_ze5i=eJ%W3@fn0Sk%4W$f>?Bzth-)bG`Gm3Lp- z`@BiG2h9w_$EQq)&QGJ5PN>C& zlut?M5j_QVQprB@E+^-jJ}Ck%{m8aq@l`x|Y_INK9k%-NH-?5P0fn(XA0A7Q1~jL| z&+*UKg@WvkhGcYCh#5xsTio<&0Uq_sV(KF647}V1oPe&w)v{@>_WS<_=0F+0-QU^K z>#V6C-MOQ0U%jetU%RRw+`OqD-M+1#fW2tsuZJ^Uyt}Q5Iu^G#gG;_1EqHX5%SSd%Yy64=vZ66r+S@vGdQ8vNttVTxW^`GUmyL*d-U7C^Bela=Rc+P z%C@vcpD#@`xxB5?uwwa2?R$B!E*w8)PqoFmn#^X>y}QyIuWSFspX&bA*VP`}W0N=2 z*6cu&*^%0LkA}@(W1g6n=W1&T8hG)?331J6|4@s=nL5_`oPoW;W_0&gKdJZq<`?zM zZ+t;JU;3i-+0RPPu^D{!ebV#-U7boxpk1lkRxBr)b`$NEElv0XNvnnpY>!6o9`BC8 zyCd*(Jp#VHod<7ar(I9YZ~jyisoDfD4$F}BxuLh+SUWtNE8P6g@;YWep*=Ys{kYx8 zf>9o$ZpFGvdDNzeH#E~gwd%F?sh|n_!s8SWak#x&o2lUkJElC-&;CgHV22kEGengU zE$C@Ug$JOL#{+79Wy61McP$+ccnTT(f`_L4ohNy6xjqqRooSDfy)&P7`#E+%ty$!? z-MQEo^=;u3_&3;WHx28MlVUTqzs#c-4_+gvL%djq!2`MHJ05~Out&PoS37eT#P#hg z732!?Ht2%ffnNhmtq=4KF-Bl-fT@TvT>+mM!FEQRv#emd96$0qN!QN_{h;jv>|C<4OI z=dq8Ge9(6mO<^94E@@G5j5{$xgtLwm4`Xp>Ptj)_c*~Ch2CmOuAm>3Qpi#mjObsc* zuVwJgsd^wMTWJdXSRMn_1E&)i7#K%>IeXcW2zhSuDfe!weA4tDejZRLeZ3xRnzXH& zrbXGRM5C^q)ZnQM-U z0)3MxyhxLF(MY`9ikDk;y!2XL(%$5vwk|%SmM2FYO=cb^hRWlk535_npH@t6AH~FW(C~;wALC!f>hr&wRjT$-?Wwu#$rq_)T#L3UZv;z+amcXfFCwmQ~Q+&_@+ z-j{CN((L+m&G|z^w|7U~U2tztI@njsAMk=dj5Hv;AvCp)O<-_+H2fh%75uTI0G1}I zq9aE1@WQsb_dToKPkc;UKp*^|^wbq;dsmuZYpFT57#iSfq#$>#0yKOz53V753 z3b$4z3NHcTNIleN#uW8-9(>|RJ38A6F9aQ?U&q6(m#K;PlJ-u$zEdHlpmUzo$+|A>{HTy$ z_?Q=dVVOMUg}#vARMri)81Y*2@WfBVYOpKq^wku5pkJf#u&SUByg1s{;2Y!!(?loB z#b5EzXZ{f_u8X`$M|z<%{M9Vr13%{($2W|Db!m63eeBDEv0lhVu*pncY>jO|_y;ty z&xM)dM8{-!)*;kwJ3!#<>f z@EhEF;>s)ZP;ZS7!p7MVxmp;>k+QNuJ(Fglh8Jx`qYkg(6_=dZl3KYi&n{kIoi)z@CVs&8NOhHzi6 zvN^rR3%ECf_gD+FrV-&fuo>Krg+BP~MSbp9KBiy!^hdOFc|t>n zQm`ou?p^s;N%K781iA%l^hndgnHtuS_zJDP`&wMTrK77iq`kX}`}=AR57qKzaO&p> zn?Dx33LC+~*Yb{ypix+Z{XeP0s$&xjNTHqgzfYGw`e9AH8T`<5 z(&fw2bgIJ${_xUh!De?66BQsstwfwWl+coB5PM{oioDDoAB2&}zi%CUgFXvyUCZxg zes=`k*&`6OyeGJTS2=2~4O*|g4YeX7!F@>zDWT(s)v0lP$sGTwcGHSu9z&dM)|(o6 z)7h}ueiB@N)o=2a{o-2lC6BY)uuVM3dMLY>I;rRL<<&(12N)!>PvK>u5L*V?@zPsT zE~G0;ndiw$H`wKzz;+xtOTzNXmpqm+aQ1c>73bgqV_Tu!N(=rN*}p6y*{Ez&>I?i* z{)=}v#M)N*;+*&uUf%3q*t`||64qfSSb|z-nxK8h3aT1UzpU#)o8%GpAU3ec`N;1L zWG^q3o4}Qwr(pVY3Uu~QN{+$fn0VW^}}wCa`91JS`vN@pDesWVn6z#Xg1FZL2Z1n>RJ=YrA3Da9ow` zYu3jGkNlR=HBA$u)w)_x}Xm zg!0>;wqsUcG(-y>{pltX&B1-<=H_gn8|=1jEGN3fW^ix0rF)Al9hMy}bU`I9Xffa7 zQr?o-7!s&5pQvLas2SPWM9ydUA5AsG@37m^y>46AvA=kCt{>do*WX{;)8D+x2JnT~ z^e4~1tUrC>RsF@wSM^s{Z|fV_ezL#RE4<@hoj1BwrrOg~2ko|IlP&rGebHY97h-+T zocqTTS@#JcP-3EYdTfXzM1l(P(GuETmo{{!hHaIa7dD)aw@YZP$F_czgpxA;%@~p_ zM+8)Y3<%}_tdI|qnt%XqyYIL41z#n^(c57P`U1_qDebP^_BGXTq~nOUJfII4Y;Q85 ziKa9%{AlrQr_iyEd+me`-sPQEAN}C7`p9$7=#sw<@=dPm4jJ|ZYjUn~#KKwf`P%k!QNLxk?`^M{QEo56(|Hh>f6FBaUt+<1lWd+;64 z!eYwCu)}A`rm*XRs+z=t&E1?oti0zVAJ&EEJ}6yzN@7D>7Bd~rz;dn`&$*5{G?v^) zN`f&AgU++5*T&Uug7X=AKQ-cODiI8etFiY~n96mIl>sB|4K z>A*579wY*I@eHu8BTuPz^Eg>w(P1;HTq#L73-8sP9B-eFhnVdsI6G|Kbag$?gQZZH zee5gRF->`-?{yuqYxtp5@Hchbn(tJouee?3H?j{M56e#8R5`|Qp7$9501yC4L_t&( zN6zfD2VljgogCiW89}@^f#P9kd6U{6_y=vp;M3<=lDD=w@YE&H*SAyvavw)!1(Ew+W$Ix*IM5w<$ z+y*fiQFrQStM;v=jq$T?;H~T27jm072$WxFiXZRz6%B1e$w))JTz&0txaO*>eAbs8 zE7V6O@>O5Kr(H^$^+x8vX5MfvQ^Vs?VY$LF$I-Xd_rG7m$&+{OSJcNtI1A%;5->gT z>Ggq+(U(rQ>*aZ5S0l{aPi z^!mXE(x1d}Doe7XVGByLZldIx6)O_Zr)Ar!n@)7tgzj+7yn5K_r32~3`%AraZ>iV! zrJF|+-S2jF6c@G625+B@;$hj=tl815-DR_OL9^*a9kv&=*nUQP_`bd$dij3QPw#j7 z!JR{Wb0BtvzOk`e|zOR8^AmICi?Hw)=S#e8`CSgwfn60rcY}h{)jxY?WeTZ zx~K&k!bMOgFWj9PdPHwnW5FndJKMpPR$?;&2gRj4KEg4C`7|G91|&bSTb(*H11uY| zA8-zvtV>vD#Q^KBEnCjA3M7MQ?j(I5Ix%g94KN)(t?#c$AEVNpTgIQpsPp zpA2z5{n`HpRkmHDSTKnTMqZf4hCok#V%dcy3hi7BK^kh zSkaa-ZnI6*ZeU337VO=xIUwP9WQb-R#k9aW7)pNu; zBMT%g=8Y<+a`fXc!>UT{?`y#*eAx)zvI{kKM$>4)rf}AV?!&*t%klH~X8Pgnef{9Z zJ^k?Jef@-u+spT7dhKATo86>GndmkfwYysvbZ_T^ZcVp!y_xFe!)3;N{grF_tDnBE zzvK(xSJ?#qHD3k)_O%=OHhF)^96+GO(qpmw5nKw{)%~=eGbItenwK&>WIpi-5 zY{+apx!L=K@^U`a&|JpT`y?t%Xi?$b1%h$ff_sjuWJ=@3!-|>tqP~R6j=#npl zOX!^T7HnDxSI64ynu+|i{XSnM<$v;p$~0*-*`8`=cd9LRw1|v<2g(X!u?2l=v_wZi zY#KE}V}~b$-aY>NAAxs!_zOM)#V=>W7(~Rttw~w4CvuMP4cNxyVX6%KGdah781ru2 zHH~#+er4w-Zr)hi-E+g*9=2`5mi}GWFSeGr>oTU*&zcpFzRIFa>n#ht`GT&mx4v}K zexDo{=kHNpjw{8;+1s3_>MO;l;s+9&Be#*V^U|Ki6yElZ2bJ?|LPMR-TY3Ah_#wWq z5pe$kf7>+3CEKG;a6e3rH$JBHPO04ZLZ(-m?s(akdt$z8}r24rw7@H z?|FXl(=i$QM*VKX@ED}8V;t(*h%GBh?27Mjt+M$F>xw$&v}EjYp0tmAI=(uO`Kmh) zjR-|N9L^ol`)%@;Pe)*?fc;sXL$kb3uU5pM5-*ksmS6V2;cHu=f4qWf*@PBNqeFBz z*nGXlChMn1oql-lK;ON!r*B`srN6%hpg-Vi%XjbW>F;md*583|-MOW&-Mr2hzt{B_ zue_{3{ppMPvln01S6_KufBo7U`Wi9cx^-7SWTW|V*XjnFygSoN+TVIgN7JWt7#DP~ z*wNv9M@L54)ltC98^J9t%2Z1>pM@`g!Hahkdejv)tkD3159*EV9lf+&=Re3Ui5meSNf8G=ZU*(&Y z`Q#53g?ngWlQeCMqMK=PcwheS3G$-nLYw zj6jg-EMPwEbl4M4Y$5mC1)7$!59*IG-)s8cp+K9*aMvyOPQQ4+*>}dq7x);DGcL|? zUepn%1UI=t)jt~DZ2hFD%@%q&XmK<8B>AL_Pok@B(7cYVUp?mGMDEH1VUMP8IJdPd zq30#9194Otc6HXKOcUvBZsl@}IVs06he5F4LLtUJM!_*3G(2qZWa`+A@M{;I6KH#DA*i75@V8$UhFp*< zg5nB);j`Vko)YC)KsBh!5DM)J7<5JAfnbF7*;jPr*8tKJi_U)Jv)(!-Pdmhz&vGrP&|@ytayl-*7lq~vK{-`-@u=` z1N8Qem${-K)@W#7JQM@CCbw;fJQXS9=l&V#NBx0Mf*Kp(I>w;LthHb>FOCB8BvDCg zWHW^ax{-gChVO71g=Ki=DHw*cP?I4$(m&T3)a8W9<7=Ak_DM#vt{_ta6(CDJ#XlL* z9CnMa&X_=J5isYpZ8_UZYgSA+Sxb*diaT0?0J6?h4;}@5K}~kLP*&)57&y&C@(g}A zA=j0_lRgO8$J8+2qyH6cuAw|OEyv?@EMn|y%UIszmB*&`xLla~k}8j^Aq6jbMOJF# zD1&TT)c=lWrH@y}IzE_tqt|FS!jxcBM^vmc*+InOl&vTa`-|MBMw%$z^B&d@y!Cw~ zzua6KUa}iDfe|4eUh{sL^}hqP++jnuy`#JBRM)yjFCH!Rqx*AxXYWXVcl%I(+Ru#vkOQ{CY!;^BoW zDo?#f^X*Go^7U`gUeG+YwE$haqh{-(rn{FlXVYjbiS6JEn>~S+FEG<~La&r4T}kR5 zc)W6rl-{Ou9(}GL?{TOgle?ykfr(ObPLisiEy$3KbLEe|=kXi-J;xR9N1}*4!F^It zAW;)YJyc5BDFU4~59?f;9TMvO;cNRkb~wNa(~`dS%UXzWeZ7p2^BS-#-M~XQi!y;W z4||Vwo3dLG&)r5{)2gsO+5aopNEOXh7Do!68S{ffHiUDra1;y3y(7N(-IMs^#e}be z+iV867jrd73oZB>{l+Ud^rIiXqHlfY1%2Z?Khd50OU)Lvv23;Ej~=u8N1C&lT<~>q zb~uy&kmU_v-3*F9vi2LqrL12r7Sz(InFC`P;9>o0BN&(C80#2fopg8ztNtd={3a(-?k{Uee!+yx@>4&GF!( zD~Tdz6Eumw1}bu!imi+3$ z*!bnS!YPuM#PEya6aI_8(NS}S<%+6X@-m#@hUc{|*8QU($IfHPy6+N<0b>>DD&#Q( zrb1uf4bMv%)#X~OH_{hBQ_J>S@#s?r-PBob6m-9b$E8BlwTWu0F)gtzeQ)q>s5M{m zq3%om<&HikkIrqa>R{e_o{1@q3V%&5P^pzsxFHox2rP-Pkb5QUTZ6wTF4+1oGyiAcjhCzECLF%mc_3;9-VOK* zD>8l=L)?@(GVkl%5c5z5gcC4lOreCIV3x!#XrEHlu`Z}oU9{?0D@)L|@O~jdSDHpr z2xG$Qwa9EiRa7uVgg8Nv9v)?Epc!P-R*f2-S*F#GWVxh7^1i-KuCd-nJLb7!$sBZ` z5Y(^vPM(7x*eHx0ZzeJr1`o#oBCvH`QKA1q4MG1RBIPA(WX_y2Z4PA$I#58T1#4kR z3@zbHbMQ0=Uw}{mj6rU%BcnxSY6Sj4-;6@8tO4IX2aXDU29SsD>NIrj!+`1|#@BFu zk8wj=MoZoXwDh%QJ|`gX9YffqMn>`c!*>RL1zUmdDdWUgX+SiiXJ@1U01yC4L_t(R z3}-L~S|eCL;vGq*_F;@Ae94C^^Gr7SUU?RwI>yA~BE|^;@2&?O%&{Xst;qed$fI%2alTi(-!i>AO4qJQ*RN^s$3IYa^{NhDxuzd~>v?_an=k0^e{@4XxW3Sf zhg-;mgNile)IO@}vM)BNxjU%>CEi$l%hzGk|k z`SPYTzePuHOZ#`EJ?LF@x3ANe*QGaJQ6q))08m@UB6!edf4!+kA=}#lnw8r%eH6Io zV>sBd=`S)iFVtgW@vwnq0z8K2K}}7H<4x9N)nQESJeIxAkI93Oe*UTGFjRg`ebLtD zRuuDUOo*y_*vC7`khi z>%Q5NeKiBB4))l&@^SlEEW2&fM*@8x?sjsVaZ=n^e~i!d(??K(O+yvd!LCkv?i)Xl zyp{|a$6n{ALtn6WB*I(y&s~GNUsNjfgVlUP&EC|R25qmXdB!?r+Rd55Hz{?6;hfe1w;}6>n)=+L zeQ^|M4}|1#Gn**<7PJDU#){`9#d2o~taq%(C-65ci=(Vy4q-S0YKS^bjbh$z1Dp>x zGAd!YLL3EcNy^Ye`}NoJh{Nf1*1?ZG5%YAb%#cmwO4Y`XYf7NETY`Btb$e^LUFXAb zIAh4Us+piz*@P7Zb4mqr^mr^hevUIW9OpW4T%AFN^Fv%S;zcTwhD5d={*9Fsl6k*V zCA_M7UGdyEjZ6zF!>;myyy7hbyJtVeoEY{&&nWQQpH@{g1b7TqG6_-PSk$2OxLJn9 zBKlFFJ>;VZxrcwNU(X{^a15}{AVmISmn+O#qmaW82Qg~60^M?2wV;3*o7%&6OLzV6 z3QjNR4qx&7k9!(>)5|jMPA+4=r2CVnbici%du$;0Cp$WrY|F7THkpfeU{1WH#;PPCr?`kMwc(E1G0cZe%vVPcxHXBo{EzW&^g0avDAs_xVDsSrX=;`>E zgoD`EWVy(FsQ%7l*|5&CuU{j1;%!^=BHmIDcf-)=2sU`3=r#1Kw_83ASF+>+ShUiT z%duksI-CoUOV&)s9~XZ7{Ofw->U|v^O||qtYP>Jqnp4Yd3{F;9F`W?NnH`B{Uq1WaOw9{*co$fWeYA!yjtqad8VplW%9&vcI(3}lm zVKdmUMqAo#p@nDd0{zj^Or32p(S&s_HkEVg+GlgPKWp{kPp;{^U;lw#_{Mjo*RHAQ zLOX17cZuIQnyWq7*YxN}+snD8{9#7RL+SpWmN%|x_WEnO_1*92mB0OK{oQ~0BmKpH z{P(m>=ifcw9f5a8z_$U9LZ#-ak9XRPyhjNu1IzN~xNcxm6{EuRMeRnFs8Q5&xEnA%q5F<*72)-&3a)PXWW zFZXslm=F6`q<*N~Iy_df9WT16!=BVH(ZmZXXu+No*>?N)SfcLRE!a}WW~w*_Sgw3h z@;Fx++9^CxNWjCWQ~Iv#$k@str_K3Tuzsn}Ty76tF|{9*(clGe__Yk2U(g`7WV@XT zaee>c9g#8`Q+#bte`NC%ClMR86VS%%0Gsd?)Ol?;R(5`k zpBs=(N;xBP2A}MQv|i`g&L~rrD;=5Mbk_Z>Nyof(k1o>@&nIKo$1#uag|RNs&R;n^ zuV8KO%rd6L;-kU0Kt0A~n4^O2G&n)Ejro&wW4rk$MZ|h`BG&w)=8ag#M6CJE_=vSF zqUZhVbtA^y{^qT-Y#I?(=PC_;8{(Rf8dEGy2wqrBLlGe#p+N6n!Ra9f^Ohc!rm*pI zn$1^p58iv#(SD)DCDG=MB}FdL^WB7vUgYL+j@>$;haJw4*Z@dSy@f|n-$J&fH|Zd$ zjI+|!epPzh*Vl>X-)X$=tF;-8QN)9>Rr4^vn(sJu&Bk%UBC^A&8kRv=FDy@zL|3LJl=h^ZEAxaMrwj&woa{K3om_VZWu zlh^L*>b*|)yKNoC1g-&ee` zhkR8>Km4g~eDgc{@mK#wU;mRo(^voKkM$q_<-gJIga7}(`rq{j|N8%+MVkG2KE1%6 z1V7JxckK`ByM5kDyxr|TdEW{t42UX)4{+>!Kyl3arV67noYyTIS>A@t<8L2hlecWx zW<0?2f+b-6+Xi0tYTNnpM?J2|u)gY&$7X6+Z}=f-*{FRS^VW@FTSaZdvVBY|Hf8I_ zx-q|LJ1%!L<4G$}^M++1TUXTiC7G9B#}!j=Sd!xBJ|mZ;OME-f)56$!L)H;*TA)UU zzfqD_UnmpT2<^tlRtMu*+Bq}{ePyg{@+|DA^PCm()U7qxcq-9W9gY+6#+WnZo9 zi0ydt83misEtl9k4@gq`X|UN|h$-$rus;3KF%ZN!4lou1g+2&d08{&mIR7PF&Pkm@ z7Q9w;h(}j!Cyzi$^fvRoe?KRAJu33A=Uxj>g4hVQY*e*ha8`U}2I_GcSjsDjP1!yV z!#HNl*SM-PwXUcAycWidi9p5JaFZ$?S4-&23Pl}rQqt}?z$;j9FTIVpDx1<7PI6|B zl6*;@G0*&DTfGkU0l%?s%sZFmCqcw$rZ)HpZ*N!Iuqb%?EPDSik1&%lI&+ z;;;naEXtM{E3bR6pNL3ZgVitoYZ|-+8iIn5Y!7i15o5q)8 zC*q)ZD?6*vLHoT#qdk+qfnyZ^5{plv(Q|=zGhtE^^}-3ifzE)?wttnBSsvOJhdoLu zgT=L#oJ&l3M%Y8W9X=w`4nbE4kRo~?ag10bpZJ(O6l*_^OC}~J`60de!i1Hf{?lkg zWK9t#QDGB(fHH=R#=u*)zCmsj{Lh-;dZIZ=cv7P8>EYGqFTPRCX;CJciA|p_=uj8+ z^3A!v{lay9_1izy-?HiZ{>wM@(yc?ieEUE@y?(%k@v6T0!)yBP%eVCE{!(`|(LFYB zhg&(i{5b)!TaE-duZSn~!}@Ycjo{3I4$bF#ppRUi7T3 z317-5Yyzj`3HUj>y~88D&PMRX>o@fLwQKtE)oVIH|H>QJ^*4X>4gK+d`eXgkfA~-O z>L2_U{r!LaOFjS9Z|TP0en&6<#oy}d|M|b_PyT=ZQNPc|@8A8)f2Du-um83F?f>>~ z^wmHA3w`rzU)S?L{E=RJ`4u(#1wR;1mQufDStF0f$1nKc(R~lWlYkU&K>Y_)cl(ZXEt&Ho|M{Up|_6#o2N@Pcrxb8=_YU4ILYn=&h=y{3W`2Iw60sn zkF0x8xyFG_{}m5&+QS6X>fH44%c)_9Ot5jLv?EPMvM`LGJ7`@Fi(S zswFV(@a+a3UHXow<1?mE+o&#dUD}p1Ev{{-wS%{Avwr*Zx~`8Q2Msf)9l6)AU#azz z%RWXK`4|5?H24SvK4YXGdfS|b##9l`K_6{1Y!CX8dTh>9kPRU_myv6bA@I6Pcmm%H zIOk>_>p)ezZMRwFF)fU_QFZ2xDnAMx`;Yl?to6oNr(qpiSC|8%*SYu}g;%X^_#;5a zcp8P8?YH7BXM9l^sBJ6^rv`=ljDTs8V|(yRY&iiq<$03Lx->rFPgdf|VS_?(eKuZR z&DdsYm^aL?kP3pK;b5qTbIe;d#<~^n7$;+4JaGRd*D)RA;}R3Zw8Tz$L2jEyEp!4+ zfBeFrQOisQ-eMXG5o<8&>W|Ws{H=VYD5;HK&Z?^v)o>Ovcf}cLk+I`0Ab%M&mdJ)T zg^MQS&7i*yE(U!eRtY`iDA+4t^JJ~Lz6$zmk2DQG;TOc@Lzv;9*&cLdLw)YkEgFxL zfVMZ?nX{@oX>B*u95j<)?A(_=OThvx*hrFy=;ym2*b&0a@GE@vkRyZeIZm^tKux|P z&+u`(E$2rOOJfxtuF~l%u%ym<&<|AzmUEf#1{o~x779JRCk*#9e|@|o3Dr$ zG~d3W>)n=qxHr?+-ngy5`04BVs~2C_cdp*itM?9d>)wHm4(H;7B9~gD(s`yP^ry#S zDILrfx_xh7Z`|I~tGDjx)xCSVx__wav$+m;cXY39(cRV8zx#b+yH4={01yC4L_t*j z(SP~B^oRfHkMw8%=b!Ke@K5z`|5rAD|LecjfB0AbhyK&Q{rCE_KmM=!#$W%9e(=5T z>BW~`)b0Iy+B-VbJ_Q^xH;sPI4==RyqU%?NSP|!gyo~sH8_$RJ{jDy|?aSkM3b^b} z9bVR7+TQ9K&(1%(YuM2Ro32n5n{?&y<}og1l&!XS);M|mjT*MeTQ*Lzi>GkfPMN#2 z=Jl<#WlckE$x~labelC>pW}p&YZ~)oc~ftjV@*9a<}1#UH*Li=F>f{2)OUq4ywn!g zMNrw4iA!DAQ38)hHXtyB_OdrAmJ9J6wt`;=&6i}yBFj0^l>r^Pl3424W^k~V2Pkc^ zF)GFo*YivDw4)w6hs|w?T11;N%^C`9r_OoNn|6Nj$G;#K{EXD&)6?!)3=Po+I_xA; zMc;q7&l8cHuHCjEuRvil7_?(PDSXC2gWAFrKl>+;#Rf6;@Rf?(k)W-??si11s9)r& zDCW_*PfV$NT_!!}kJn^{QY4 zVJw7CzAO&ZykUJ0)(^IxALe<8E2($81~&B8b<9s0_)4`Qmt(PxryY7-((gEbk`7l= z_I~Eii-=G}tiuQ1nIlGedsqa;NI5b#*pNvff!N?TW&0S`M>vL-=(Fd<@Ywh7^+n0E zaQw2IkmMJ>7TTiVRPv$Jr!Ay5w2G}cQ1zPx^0^(uwmPQbQ|obz4)vaTTf{(cJ#(9^!&|x`sRzT>g&(H zpuYpJym3``?%vgcXHV0#Y9|u~_W(-ban)&m|3G_p@9EawUEO3Oc=u?gLs9k4uF9p0 z+M6^wytu2`g8r*C}o@AcPz^)-F-8-J%C{pfkU@%lC0yT7jm zgVkb@?{x=Oxe-Pxa%d6ikwE^YSDy0|&p zFZm&NF||Lo-mio3N`$VknW{j^*)cY~-ErRB2|tmevoAM(Igf$PvECqwKA7+Q`yB48 zYcX}aeaxrM6ENgWn{ydFWJT_5%klvGWIf0t&TZLG)D)?6e|j{|!A4E!gIo<%Dfjls zwmaEoAEP0sv%NeI3C6@aBV}3-bVdX3^`9Vj!%AvssKWkj>LvSoqiCD;mYq`^=2J($ zUPlkSAdS@F^Z-3Um;TlRFF)!Lpj^pxgj^=nmR}4dB}MiovEZjM&?^s*OfvnCMYp-4 zto4*lCGYuGQ0ZevzhPs_PLRy-Qy4Fu6XoPU`M_qf>RSpMKauU$^=;#r4=YB9=r#V0 zh|FwrM3^LU&a>(K!KczR^HWmiHNdDL`z-tzkQnHA3^P6B%r%Thp5r=Gamr6bDB@&b zP)*Jr(5n#rvCR09l9jxylHg^?nkTrc1yGAu8tB;?*e~?y1)BqKp22fW2EWZe?u2h*hFsJbX&eePZ`O1S>z)9$xlGajtg>Uv zvU>IE*Y;X!@+(WvJnUwB%uX9M*Li4P-xDV3%g$f-$FShsE1|+$(G%|-10*{LU`@lKYkNMt_A{e|<*o1+xPUw3Xj<=lx;8NuWl9{B$xgPOw za!vM2Jv9oOb+(E4f~*9Ypeef@HcZK*OS^TMkNVK14VfqRe;Z$Ld( zu4PRN;}2ZCeUcX0-*Aj`IY!j9=6UmT8{~BRwpZBam@j#~!E5jjIc`at%WNZv&vxiv zfCf46ca8?xH1+WIP1p{skIY$k_yNb_=NQ9wrB3-?QQ(EWry3nON+bCkEJHSqdRWG> zeat^t8x?2WHp$SdXcB&mUEenka}Bo#b^$UZ+(5 zgD-aYaopk_vvaJY-&_KVxvNfVC++Q7hfkWmX@?VFRrhm{-wC|e2#U|xD5oN$_lovK(6E~wKF-A zI`LQZT<17IL7S`e3)X|JgrX9_No^3!lQ`-_L3_pxL_k@0|2GtXgj#x^OSm+*-VtZEsDrJ)I=BpwsD8hMI;=CY|5j)?rBZmJ8j7F4)v9cXqVg-BHIEz`d^M z9<|&D`_y(soyIJ`gidtW1!#1zj6{GwzFJW2`%gj|kGA4pP6PPha~%h35sKZ}WN5te#fVW-m|kdvtwW z6NT$+jE=wFjOFC51GcT=xOQ25UVtZJvtrAQ*Bm99KP&c8Y=y5oXQAUpbvl;HevSK1 z|4IdV(ZArP#+_EjfZn!>abL!K%|G_D-S9a08m(YR@~pCHM-9elyloYQhlbH{v+o!e zqYSz+?eB*RE%S>BV_*tIOrgESVgb95|1#KF=f2u*3JN|R3)7PNPK75QiCZCWqYEQl(7}s zE0(E?BD9IjS$*(#jzT_DqgZD-`^zt$7%^TJyg%YvTXc$uCxC{wxHhPvP>Uhza&<<; zkY5e$Jcz7`G0qU`3W7X#4RMBLRI2m=p4jz0&sL9*@b9U|xY&mT4T#_e6Tg_4KWw(U zjvnqyr{!?Z>H9)}e2l1zr09ka7spU>Z9W`}~XUDyzKojsC z&mi+&yDgpBVr@_9&lbJifo@aFl+9YpMo^QE&Dm0mb^(@}x1Hwbm(xaNdrKV~u_c?c z1MX#I5}NXLZ$f-So@UZ2wx=q5z2hUHj%EicvqNq3yo|edv~{?zPk-or`X~SJbNbS6 z{f0j9@egToX`qD*R+S%3)8^hhLiFVoi zwVkxpOu0t4q-gmX+2}~pUHW_T@KAS{hdZp*d#sK72Ak6ZHm9>C8_F_a4ym(D)lsO? z5c}@=kP+~Le~8Pw=AX+E@GWE$?ud1jn)mH*guMHVWV=2AoXO#)2KO{>QP%XRx?bvk ztuyRv&BNB{*gobRdwzIql(o8+$4AM(Ui#Uvg0#vf4(lPu&TOZsk5zw+`!wczD~}}} z66S3)%vV&|w4&N7wcfm`VV=%yXrAp9-7*p%bJHC(Yc(o}B>nd!E%+ry(^dLtV9uM+{MmBU& zflqCVf#Q77ug_0jd_5jDw&y)Be0?Mz!S2`zK$RzvHMlc$F#Cr4L&b+Z4r=9<9#80A97hfnz19dP>6FKMOj(&|N`Fmy3<+u@ zWmVA}g(yDl|IKagG$nShvpTq3byq`RkDB$#62Tn(&jo9#31_ zHa#nnb~pMO$PwYGfRiFZ{+~uiz$Zvw?;Wf4j}9}ZnF^7^e5IAKN*;6o01yC4L_t(n zf8^L(SJA8cU+c`78s;miTxr$!G{RLr$oHDle4XlTNQZ8B-NUW!_ejUiT* z2OIx}xC`5%D_hbP_zyp|tKaygkLfF){ggiaOCM3g=C9jnr3(|ubQgY$v7o46w>RBt zv^8zC!{%~l+G-cK0%3rk&J!XUyz%(P#-~ZKGKO*!a$X|1sD(`UO5q zuKp!o2~8IzR4HB4yT@aUfG?(s#|V4Z^&LF|+!45l`x4EYtJM4mTs-1}N+ho*I^rSG zY2PR%bwiwaug5{un77WdaTagedEmB<96Hx{9*i2!T*yJ53Ob|*z)Pe1WcVe*u)nD> z<}Hu$fGuy}GzzWlWE~^9>P=6w?edW6En9YK0;^O@fT84qmZYkV?XNh1d`VqI?>LmespEXW4&Vo zd-B#NmI`R#YkS%TKJ~T@XlUQiAg3MQpvJ)FaSY2muw#Uho58|WS6B-jkFqZLpzeZy z$=J~xqIWGNZC%C$KGqrQINO%8U$8I=kHtc}O1>itb_S!6+GnPKDYV`pya|^-+4_@a z$lCMyTjVn2!#2|uY(uc#fPI3~$$HejyWz^48dZ0U_ZTq79rNcgv@Iz*)0KXZtz$BB zyv-XLY^HrDipr4J6oJz^<|BQn)ppil0ptFsf1e~{26jarW?wlz0Yi1Hn@o^Dmd#Y^ zN%p!)*f7Fr^_`~AzB49tmSOc6(F$YZ_09XDj0rD=6J839mX~M0@Y$Zg*FFtJwN0aj zS}afONAd>h>&Bn~yquqT;2J5kTcFk-2T4{Z{>cxN>ng=#Kehq#IZtg%gKS-e+Z^+h zs0e;}$<>K)FNwe<$NLzi%?Ij1M{Or<3Pb!4vqM&}j@foA=LOZ#;z=isk-N7iBU#AlZtk>#I5u-{&0FmdQtQA0*!|9IHHRH&dzk z2N4m9hzAX4Xq+*cA%->MKDyopzGZzj+?$M+XGjb04dJ9L)Pe~<6LNbaH(@+l`xvoQ zXs!|!-W>9rh)x|~|HWod@P&<8QRd@0-IBxk8uA8T4*k6@7WcJX4s;PbJ6-70AADNB z{rOMnH$L+Tec*%7YI0$!=JF03!Y%32mUMBdU~?Edje?EgluhE!?o=0crhFYVwsc|I zian;bn2()>vUoBVed)2ER-{0vyE@8HuD z{8l1IkAXUk7O~{8n;K)kl%H_y9@e)fO#g29Lq~x5UHMdglMW75og$0E^q9_y$G{Nl zOoo?QVwA!!J>xavoAOvT?|ZT#Zb&q}RajKt-#)A&@Rd~Q7C}HdhXw@%k?xcsq=%A@ z0Rai=2I=l@5C+Ks>5icrhM{u?nE(9V>wS*)!M^s{UhA{go&Q4>qONwcP=$VDcZ2IY zCd*I8adCcRq~o-z%!BOobCUcGfbs1vBY$r!7WQZ_#ex`hWYntVT9VURgX~PpH5-MIB9wValWTK zw@KS=D;dR6Sz$WX-p|bh_(QLE#Hw8VhljL`ief$iYpsnnk^3UI%dO@V4k7O9sQ#BH zSGxcZ{yi5g#dH$)bAbD@`4C)W9SIUhP@uRp+O9QT#}|{`{u}{Xy(56*`OlGoP{_ylJ-@a1%^oL6xwVrq-WvFFVd}QN#&CW|z;#+Vhs1C$1$y{p~Z1 zFad1yL??bA(j*{G=c~;6_n-``xZoP91!f`btx<-Ppt6k9seKBWaKnTJ^4q&50SN`k zTgyy**EhQm=96m<37*iMK;5n{dVE=8=j)0}_uro;edIoX!8*GKscV0=M_&$0d?t8W z8B_3vhVGL2rCha-B zkwIjyIP}-vWKctN@r2TaIp?ug*WlWS@X5zaI_lS3Njg)yx=P z;P~ZezrwPZ4d-d}C4U?4%6`9&nZjy6;{1}k@Fbtf0rsoGvAb{wCvP&#!+Z#6MqA6 zn%YwZ*p7CX2=g%5&skV6cglchg_0(utIsWy+F5Z~NqY&K5=7+r7jQtn>E0q)&nYgW z5>p#k!BH_E>4$eKxN#pT+-{dK{ao`Sck9njM7%t{Y8UMWIzcLiJZdJZzFnyOm+m?j zkyyW|dxc?aP{rP_ow6KQ8MjuB6Mx{eB6wf#__2vKdkE4RWOyI3gu0WZ5DrTq+TvWc z#po`Y$>^UC=sT^p6J@weE$CYth3xQ^uQr~Vay5QU_`ap`PsK~13r_{+Xt=FkAd*61 zt0kHu5>-oAn0Q6!u^3uok(89-d${6xAI_1Yg0PP`T9_dgOJLWq{4NR{qCiNcuG4K1 zIx7%OU%HIco?Yy!E$IFw?=#4G1mSsy@^@joy&#nO=+HdmK)ki-zAc&z#_4pIr*hk! zR*l-?qVR$VGhWIw>b=^Mjo}Y+cuYmDNTbE&QhdLW$qG`Fzn(nj`dfPQXf6z5abo8` zi+cqDnAs7e2q%*cl(Jx0as#Os&E7{*J+0{;Ble7XI~-gK_{g`9xQHty&C7ZR8J_#+ zDpVW=<2=ECov!KTTw_Es+1BSCV}TEZmGNc`4>*MrZk4))gIq!96woz z&vsF7#&i=c8pgB8%YY|KvEJu84IEI@v&O;*KV8}=zaVy=GC*P%HWGI{Xr{mbGq9b4 zzZ3!la)H7|_ZKf0ptUd-Iyk_42tHH06aNwTop4(F^vG5}@k(hW$zP>zZ(i^gftPKt zO1WDcCqw}XM~)hsm0f*M>1unO24^FDaMI*9rjxh+mv*ytm`tj<-iyN(vzxvuJ#qYi z^6_vfC2m+l-9uL^*6yR#4)mnZE?@o8mIu!u6pQ!X0$yec7`sUM!l$m$;+M4-iJrbH zMl-6Wi_6{@?111$Pt<$mPYF4SLHM_!IS^^OfCa`FX)o2=!^N11S}M?_y?5!e8M?_E zs?N9F;&+!nx{|&0*H)Ur18yU*Yi8%_hEwLYwX?b%hw3=|y=^=Io;b@~?iaPOQ+>np zoTSnAc7PY&_wQR9IJmuSyRZC!pF!t30HMt38Rz3KqdCNM?~#UM6lhb<7ot?8j;Fkt zr}fLUuUHQ%g~jOki^(vtkRZ0oQ%xbgV=UCc=1YO zfjQx19l0TCb^=c5u{zOEC1d0gLTlnTUi#gyXvWy9k5XEgaAc_oQEm4Z*>=RTZ33UzA{pg0)>t;PV2>|sb1u*+Iq$8A}*m4_KSwg;wjRS0X zQ>FJ=(W~h0p#TV04 z99T>*T@)kwLoCPDzsf+VcM)}sl@c8E;~3#ZFdvuhi#snlDGlwc#0~{F?dC? zd!?0FQjNzXR&AEpVyq#{8G9FQsz~vsn;nR=_(XL{*8x^|1Rt7L3YPu6FVo5C^==LI zm*P!P^{;FnsSaV8q&xno2d^`GUy#IGW520@Qc z1!V%vR7J5m89!ZW(=t8^4+dtww;8V7z{n%Fr**>(3+58e4%m)1*-XJpj+H_sE?cdb z6K%|aZJ(G?U%F>jNjIFQWWx1t18eRPVnG(0VV>Hd?jvkRjF(&0=xb(U`(L5D;j225 zsU-Kg)7cSp`wtZ2%2CdwwHv4GDOd4RJGNFceH!=eW6Iynwljce^UW2*iVtg?Vu%rP zpLFX(*G3e_iLW8>el`}~(1qlaym-V|f4L8f3)@=@b~m|!Sc`neAVsb31Q|W1T9m6V z0fOT!wd$;yL391{l4qPI_3w>KrzHk)zcc4)myk;be9BxG-f91Rjy;{RZfjuk&&oMt zbb}~uEVfa|D0r9cXvNj_xoT1;ubN;-#l0x~Q=t4nfg2-E$rs!+t~ZN4>bmE$G#38q zRfAq&-PM`e#4^ln*T2Mcwkz+%fQOh{E4bx*)ek8Kyosn_L;8yp4#3q`SR3l(p6vf$ zBLIiZP!M>-$~fVK4Qotj(>Pbsr}LDg{fI`;q2ov&0CeD3m0IAUFrVLtu{9T*08>(a z8m+Zh(%{j1XxmED7d|WyczpZP*Uv}Rsj}_Xv9Vmub1Ez+88rXFwIIC9Mpj)Kgv2#0 zsU;R`vClHgspZ*9?s#8Y-Quf}|y=$RV+hVa@acaslOe)^~)a%_#~*+-~r_xM5sbs}g1`KM!T7j|uPh;VBxVpy%n5;58Hi(kaI6tGQ{XZ;# zt0vGCVVTp)7TmgCk&BskUqCfU^(_X`ctRe3XXgJ)RX?0jo@pdOs22O6Ln4v4T6&lL zv0%9_RGS(cKj)4)qkA|4-`IhIHG+deXW^xtr`{-Fjr-U8nWCy+jgr_#Q{2eV@*tgY zT5@XkeAjc0*4OR#H&_%i$LInYptRTU`0$IZ`hH2TNR7M<%Gw=W)}{R3V5lkeKK3(1QH)< zsm5Ue(9RbtZs~ku%6C5s$gBfF09n7fcS9k)Lgr?cMn+XqFFbaZX7NM}a78_ekGn71 z#`IK(J#f?Mn1FtKnZ*?z#`Gd9*VG{Z1{@bGpJx+{Yl^z-h8KjR^$LwsDy#M9Y!5sx zL#M7&S9V!}2FWq5q-!8teW%67o_e!3%`cwqa(NFCt5A2Uw7SI>XDz^eee!25s>8gJ z)|&4#X@&2gt?D`QZ)C82L-wjp)#Z|rY&M#4_qE%LtZ<*~%-hXw+Q1? zeX=cip)c%##o_(80(ce9L2OjB;QeW98z*{oSIM5?MeN?bu zSZ~;(8aJ{X7L;s!CaPWm*CWq!ov1O%RFz8p-{AI~R7PpA#>B!B?6Du2Zsrj)y&p#P zL{1{0w2izt$2Nyfe<|ZW8YQ$xq$@_$5}&p{S9`hLe!!2I+7{Q1Z;k)?5g19qXy381 zD2OdS+{PU=9QnJN)7q!`;1WbC+1tB|*Sh?ZJLv2AozjK}t3&*25^vG79ldUJQZzu* z*376`qnJ=@+IbCMsCk^iZr4@C)~SW|AR*@R=vtGZV|l;#WO0&0>gL76(q4dpp41uR z!Bk9NYFE{bRDst?10EoFX;tI5qy3PTJ5kCisgG=HfAPyUxod5u{d`+N>uFK@;&dS+ zKdQ`pr&6v8ond|DMcR|orpq4lvdD`fZjH5f5M7h(QFqIyrB}9)|D;A!yREHoeX4)M zWk*3F18AcD;Qj4@67!)C)AA-!#8ak(IR4j?bXPKxzsN&%XSrc-qLcq^Pbg zXX7Uq@2vEkBEI`mLBybU|1bM)VF#D3+x*7Qq;J3mj_u}8IUf$7;wD^DC-53Lg33+KpmsbX2ewVeg7y`Imn;a&2@>vvC3VfKk~sF_OksIq znrQuB!c4)Oi;UC*|0$q_M7m_wgG**a@Wy#(>#&m9WDnJ2)$1wE`JZ|#YVPYOevv7S z>rWfT&;xYe|I#$$^3YtrfJY~DvC%jP+7N!TVtGcbDaXbPlK(J2p$>$$Ml${N;w&?M zXz0}9$78pw>7p8$3G`$$XM0 zE?PSuQLwed7787vd-~8c!AF!;oT#7YfEODtBRWjh2fXCCk8Pn!)IIFhj}yn^8yn8O zy3nRfE5dVVR!Ou3d?O7$FjSVLND5P99e#^Tz+My6lYo1x@KX$gf6nt8`nF}l^yCV= z6UOJ96qh9)&L^k*WY4KyYB$HwaALwxKwbIs16ACW-@iOntmR#5!H;wcE4h2-9~!?2 z?fWW7(jjQBJ;gs}ya3uS3p9b4r^g7xjuJn&O-OAG>HkM68&&mmEq-mw+)Y+Inf#k~ zAmM*D=Ce``mVb&H5!bIBYr>$l3@xfv5%(vtD*CLU^Y{hb?goQ4ZWA$7hh+|TCqLyi zu!3%Lt?H~^pEfG@O5xXGQI?tjL7%}2$|1GBv><}Ye4xeiEp~YAWc@#^MvW32$*p7N z+!(4Fl4X8l+eozvQE#z;11qT)@%MTv)xXxiGUJ`R84I6h^>r5&fFOcoYNUbo7U+8 zdc!!ClTo5y-=e76o02@@Si^4IH%?=+vsY$($@Q+vO7@UhJbvovZH537;Qh+@{<*7K z*)7eI2BE8KO+MG9!Cx30S}w~F_(%zF$ocBc z0fYaxERr?nKK${{TB;mG|@>D-J5Co$iMsuEzNC~U#cN{Zf zI%H@y5l!-IPEkn?&tqyJ;ZRk$L(RW`;{ICJ85&o~G42-GzD)!z8YftJLFI$rog1P* zs{S(sP23d~rxuyM9K-(Q@kFKnN&5E&`~Avbm~i0z>)q!>@OP{Ly@Ov8!pl*)YL5L| zni@MQV$JYCueBB1_g8WWsprnJ?N9XLwx1^U`Q@{Dg`Zu1KD4$AUS8iq*0Wsn*_I0I%}F49s%hjGn%Db3><8mu z8EJZ*;YMZi%uHHFe+-L?YT~JPZ3DE}w+=mT&Ly1BKK~_sUE6IEdYAZtaJ#=l9qNL{!&RbE_Xxh2LGL9>7NyJzMXhALPWzm) zLjSw5#a{mHOD;aTo+i;AO)@No-E6`_ai5qC=1ymwEI3_#vq*eXsYpEX10JE(Xz$dr z?;==b;rM7)3h8#Icn!z>D@Qod!0yo4QT(_z1RdU#ww&gy(EeiOglld82?r}NA{Z~@3sp@fv>d&7A-;I;QVEZ-r9TWPYna*OKqZ<;TE0w@*yq7aq zPfD$6*DkQfr#I5~>8O<6-d2g#yol{YaBPb#>TS;sOv34aghGy$4Wo$4x?K?5j4pG2 z953ag@g^b&E?)Ii0*y~P}7q%6Cj`5 zMt&!>%^h8jZ8rkRuuqRaoL@)E4VP7O-+Nq@S#=+NHcJ;w(r@i`zh(3~}$Ks#lPZ(MWSeDMU1KXcWu??m_J=S{eOj4os!CGk90@PxKjI<*8Y zU3F?#aVglvI;l|Epm6CYr+#_8SiaYj@NXBIS%h2cWSQ|iz@pJ`_P zuS+(*Rgo~z@9Q;1f{zLHxXlj!d@>^uFm{(vZgY8ndeT`)e8L`UNzY8}{WDnH@EO@A zFR`e{wU~A8Qsu1G@ZYwfccrZ#Te%yq|BBBR4Xm4-euafdCYOgHc2x*b>C|O6;7Hot z=td*!r`poQtUI}R5%JPk$?a$3>^M7Vo{m`V*+Of@)rZrzw{?z3sdnvlS@Gw#(ziN^ z;OM$caD(|N4Xg@~w&4%rs^onx(*5Peil2Db(}B$K*MpIx>XK>XrZWk!-tjRK;CYpR!bDczL9QeD7bNQH~>#`by|zf3OH1h+}ZiPlWBX;eA4JJ zqiQI$pR91JfxD-YJ5%c-o12nv`yZ);w?KJF^X=jM>Ypm5VWzK(c#=1d)|Yo{x0R4n zao`+O3T2Csy76zK@Z1Sd;B9U$vU+x_O{E?jZFV}VK4IHXmF1!L$K@l;l$yf!BIt{& zMuU|?%kAOKQYQ){^9Oobg^8W&X^u8^c9h&SDj@d-jF6WK*|Rm^T?^!to>ax!gF;sk zdL!o%>!BBYm$QHKm>N*N_K<`A?T-pTS|iV;(NN?)`qmej9KZsI7x_Od;1q`3KJoj+ zfU2t5XX?5m1K)B0=fWWO@0Q%0a-N?tcW<_HIK3aMJbq#C>9|Ij{hzTLaipiX{FQ){ zKH%X~D0H1u0t6b10BajL^gcqp97S{}ihyv7KMwL=Lw74yJl_plEa>iJ-|QAJol%Dn z2Hl%VKJ7NF09;__@FiV|mDCJe99cHC>Nsx&`Bd&R-_iXDCETO9}yo|iDT?aCRy z5cklXFG>T~4|Ub~nQ;Y6%JUx3XUK_nDg0zS^om1UNFV>B`Jwip99J^)NN=P%0l=a- zM(2>~CeYByt#9y7<&0R%{O%(EC+Effv%&k^3zi9DUDWetybTL*uF+!X{i-q!BJvE+ z?J1dmJp7kGBgMLqZH~$+xB;>>x|hzXchzL)g=SrT zSMtrpdXL)$k@nB8|CkjL9%R9jjKr!fvL;g88E0eACtkLbwBI zaIklQlUCs|sz_oz%=Zg&B4Cd#D$sV9{)pO5yG+KYSpFlh-7$+|@rD_W?PO>cfGN{D zQ6Svrc%!rl?SO?Pd;5#vJLt=94^jlnMbD0RXjo43O|6|_;nn?ikJIxQkohY_oh)2c zyXr3gQv^4SScq*sSB5w&vt57qRXiYlvM$*0E!n#Oj5Aq%t^Q%H`Y{6ch<<__6*Mb?% zH_9}Uk}o7@zfcrRe|l~F-@!9C3h&4odxtF?ecb(xbGKh#zXQJbuL`lXyli>@J#uut zP<(3cTz%)5P}0-esO#}SZ9fu(BaJI{`-_-^+tGe?w1^H61p2arrkDzq31T*Hco88( zcQJ8Rq~?bWvW_+fPIb$rlSr2o4Oy-jHzJ3s{vanuVLn^yA5b$BO?l)k{f`ckO-v8x ztkQ*R_f(t8fS^U4C7_pmPx)%O-ViRz5X;(>=dq3=DD1#QoX1HSVKT7UK9%2hIJk z82euP|hPl!Axe%G#e&^`jhA+^~nNjupJhk^tPcuKc*6R#dlM zn)p4}WNFf7wO_J7{mVl`$?Rc$h|*RBPZT^#8O#nI@g4pBxLh!Sg>`O~`3fHYOJq#p z=i50ieP6l@<1-P6kd-n6Ir_rm!{# z{?6V3Z#wbpr;Z_e;l|B3{8d{()hFbdNk0+HTF-#6%;DN<=h*W0%c99fOQR5$HsWeG zI`?aDI->_#Crk=_dMsm@gGHgkQA@WT5Sqb&G6H}aR{e;t6zFKbU*pZgTpvfz1O(*g zY(JgYGwfB)khe4iJ-Cb(>o)QNsJrXUR>H}@tw-zbx^mmO8~tij^vAhtn{k`^nfgG^ z4#BgloB1X+kL$^Tye%q|8miuBqG0hUHn8!mtD$rLD1b=%-TW`Uc%p1~sYfdM9S|wj zH1SMq7qazxVe&D^K9sePoK7Vp?98=whf2g?YEhv<5Sc4Y^?42Fz+ok;b++CnOH z-B>=5)70-tU1z$vC=FMsw@4-zW4xS4%iozkkLyECh6__+@Ap$B#w~OU!E=lBIzJY_ z9uRpUj%C*V_$UNiOjRJ~?B}okJwmQi@JH`6oeI_J%1~E4kLms>^qDUb7oEuyh^`4J z20pT)Kc85Vc%9*Qv4Hnwr(D;u7~)oM{0};xEOp+14_8qp2j|o45pDosN0%Y~| z|J&<7#2Jv|Phx$H(JiK5-`N=(C&}EadT`ma^A@t=)O(~zp5qgZmI4{8M*13q9iT48 z*YDGBg1O&Q!#QFHH@8^+s_dl^i8?$|RYzxAhA_o?L&@kJu{$@yIu2L{_$(kVhZDKs zyg+`n5*b3vZ?bjg8c2es+g*>5*hkZraVAEX_eh$?BShY@YDV2JnAT<|#8|_wIB4)f6heox?_y=q(Q{=j>W4BcY&}?hCb0$cP_9Bz1?ER14mWI&WKPSjyLsj z{f5Ye`bX?4FsVl#&G;xEod@h_4duP8kJZL|#!SJTqPfy(qZ+8FZi%EmN{)WNo8`; zu`6+%HWWrk-U>eS!q^ZP*4y3ra70}!BqrmHApU1AQ5tbsC7zZJ>%brG&BKtNhQ}6d zp1CAaFP@oDfxl8r=&UUNJd&d=Ad=ApPshIz427ylr`;yd(zjTG~gZnh1@_)2Cu_V{bwA+zb zq<*#>T6?r!cNlj$Y@5<-7Un;r!4SXn+I$cI-FQDkS zY!X%0ueDNb!oJd!EWclRh42DX(vN?*-qrPEoXOXq4Q=n$ZCKRyZTJPb-qO3O(yM_h z_};in78W4rM$=Ose{vrk?YAu4f;WbNGbI5C0iP3Asq)(=866xH@6Zf0r(`Z3W(o>^ zMG{M^bI+3S|6rm>Kc}9x`5CNmyQPZp80$WbUNwM@e-#SpM>F5H!B78S=ZHNs!8LBM zRf8~RNYCkgU8V7BQ5@d4uC(}C)GvnBeI|2aT>Jty@mso>!EKJl0+yEQ^!Y`E(5T-s zugb?dZr8kuq>SPZb)cM!p0mH+sSOyx zP53!NVt0%zH<3s=uwzS$r=ga~pHib>H7UxGL=NxGKCFLd*LB>ru^P2TYjGtCCGFk# zZsx)JVWkZ9#ayDJtX_rNyF^`oYhr<~y9)i|GX7!vv9@gZaV*GJS32Z=gd|Ix9=9+a z&gC|Y{}aCAJlf!l`dvdMA3v;s*s7h{R7)t!+9KJQJrr^EA&@=`oOtO2P5o${vpve}%d0Z-JQTn=kf0E>%87o-WtF|GH# zj%-$b@caHi2yjw2G;HR!bW8DET$TVI1g&~nDx@~2HSB=hFaEsF=o$L<{_LG|x0SQ) zKNjb?k;Ttkf>3}3lZbulpb}jjqr@hah&AC1rd~y0VcMMia#Ivm%SZtDNdkE()%ot> z95-i@LK>xnfdQmTh{2n|OW^B-pqgYQWbI36VX|R%Eyxg?W9eHMJ5#GkM^VPqXHTy} zM|50Gu!3o|Zws;815&c{a)1tuPKG0Qm7Mk!Jg?pD>kImKI1E9kKh_2Ke<&{Nhy&z+ zcU5aM*8UH!KDz}BDcKNahn6Ybik<7{Ywz9I1InVRei^=eG;fl{xU^J+CxRy9DoQw5nT=wf5230H}poT*Vv|~r8Pm}$xHm&3u z&_D*5KsdKaJ(t7aGZhT+C3r5Rx~Vo54TfGPUY5Wl=_R6;ahHhc>Ej#OgdMT5h*Nnf z8%88=nPa)*pci@UzC?95fTcH1OE}aE4{-$aR?s`kwPr-0@V5OgK8FNy9XoKe!>K1X ztWtn_GdL{O9;6(tK?Lh~^Gtf)%8oe}s0~_LNpUAoi{Wzf^QkhbZvH~Y4^h=T;cLEt zg;xe&C&nC74ev%DVL`5xzmjGDkk8C~zxnOKKxOu<=F$!5c|H2~fa{)gx;=}tZyLy6wWQK{V%gx7D?EI2?mJ8Qrtlfd`4@ansruMqoa0M zFVwA^NVdY>5G^&1`alZ|vN~5c*E0~m^j(t3KCN^9Nc0=ivdFrTTlZg9*yi{)bUSV?BApaaZ97$`!D*x67l%@N zY#Ajlsh&GoWAm}zZ=v&;Vu7H?vr?-E{0VY{dkN4Tmd&tU*ZWrWU7xj(GEC3f&EdQ> zM(w+)#V}cf(I6r;p96QAJv|$H^;)d>XQQGEVdMy3rqWYgmygmZcDAwdGqfW**VgH4 z;xns=KR$O?aO5gJ58MfY?sz;p>8eRRraM5sbxWmR<*0IG7;$q#e-?-RLH4mqm2r$q zRICg)o)EH)&oV7#x{lujjO#Jh9sM5`z}wt1dwgtH-&u7q*KBUDv^iW<>r2Re;AG1d zVtSNgh(E-5(#Sz_c<8G&N*NytI#XzxEZeTx^@wK1)ZK;uyFU=)G5;%V<^y*Q}k^nC7-)mg6`F=Ka*gglBGv!>z_!x%EOV%Wt@Y=Gyu={+1E6-3`!qM zG$gleyrgvGp0_NEkZWAD)6;;W!!INMt5E&PHDHCv7{h3$L&FCF1^o zOD%~uuyaq)fDdgzl=X^xWnheu3w26mV#J}A*V77TFqx%|{qk($AvJsevbPbO%lffu zF-hB7UT#c9&c@hYu4cY2*nWTl`rqU_?;vaRSE!fo{r1@&wIP!F2P=EPgNgRq3(~n~ zOS>)7qXi|F?&@8X+=C6K+!V`N%Bf>Rew^|6jn};|I5RNtL7Oi2Glq*(C^Ow60IXFUtEhI-Tb(C`vQ)j9ew;|jpYs`bl?A4J z@i|ZTU6R7ayn2mN^QFXHU;M^ta(<80EveF%-`nMw`5wji?Ty>!VC((I)3)8Of8yKH z{`ikX;yrL53+ta&zU_S5(|vGgZ6LRgN0V3Jd$Yh!cE`YY)c~@evg-jyLTghZ`ZBNF z8`%uQ@rTiixB%(hSmz~iiHVg0EV_{xH#sBM>_obY)s`-8Xll0-Osi=E|J&_l*jmln z{c_1=2=K8KGf4IRs3FQbTe;}d@|}gYoziumHhVPdEKLOQBz@&PHTdEeKNH0h&Bb`^NkSBXB{ z9qsw6oS+AxjqW>CDT=)~f{oy_`eM-MSy0`JWt}8l2W6@k$2$rVs73XE=-*6$@aiUe zLwA+n@_0&N|0}t$k{g5!IsKh4B!~5vBruX#uJfzKo(BWlu8QVukbk1)!*XF6mrJTYHn$T7QmL-DT4TMHyy!b-7 z*67>S6W#w(TXXV*S*i3EeX7_Cw&kB$6PV4`Y_S$&A@)yPvdS#1X}7q}Gm(B`+*M`< z={d(-_v`rO5Tf$x7ow0&Qz<+i?Uepr6@59eRE9WWxDWv+p zbC{#q{G{5!jPv+(9MRcA+%X{@3scEq409)&6W$jfo^jWi;N44TJKon0JXy~hf&B}k zoPGRjlv7J&Q+RP(6797pKX-P}WThqDtZ&2aUMqze+@5%LUqifWI_!u2)5>zClO>6x zDXjx|W&4J%!xOKFt#0q8W-c`6d z-{$as-2!qVTT}PX<%L~sR_Kt?kH6^=z4Hk*JW$5!vCV~`wA0vdY{`nVcnF)FLvbh! zz*W#zTipZTX(%L(OY*S5x^5Yrsg<;irm~sU_*XY=5vT-|P6?H-a$SqYi7azZi{ED! z@>-^~wu80fY-k1Ys5Xl#0A-yjI?B##d7@mc2_&p@j0!UC{g*vX^pX$+_HSXM#-A2r zvPLXvcozwe^Cg7(gjBb?lIk~Ry7T_6fVrG7)e2X>?N>;L z&f9YuE#0SbhZ)Agrho*aKAd;;XFXNk&RI{4(!@P2uvtrDLiXX=}Es>+eE60%}J z)Ai2QK@w$h)~4=8{vhKDkpd)w&1=xiQmF!B#b2DW%J$*$Z}4h5|h^Xb$`}iQaP$O4UoLcm&)_3yVqkhu}XS zi^__7ed*l>z*40@`?83Q0_ZLBK1&&c2plbdQo*AzG%4wkb-_u=1&urH2+gC9Y5T8a zDPiM+bQ>Of8k;Ug-wDW+(+n5{l5d{A4C&&>*|PqRy(MF7Q`GDzPMrQ3J=TWNLgCf^ z45Jan!P>mdYN_{^>zeUovGYZG_2IGPi*;HMULus7X_Xkji0gpgl?!|-#B(WW!$22H zFh`<>fBDo0meABM5wCYUSd&q#6N)1`ZPj_QU!3~{j_TL~h>@8C0yX=$L>4;ezaNeM z;ETO&&Z&mZS8TLv6_;=S1`LP$$C?o|J(DejO1`xNyf+`U7kN9^9xmkM+P7E_&VK1+ zi^veFx1^?WzlJ9@PC*DUw<7wY@%5JI6KBiGGt#di`_-EC~w!?Y+==hZ4c!sxW z;N-?RkDJz6cb8Ot;wW<-W2dorUShnqul7Z|gcY5uDqMP`-*?@0CxW z0-~9SSoY?ayV29S80H%$_N~Ef(A8_eCwQIWH3DNgpsu#?c!Mrsm%3iHzWN;t*3Uss zpf3F(1SZQpvuH0nVZDRfXP?3OJeUxFJHe!Rf(&l7mb6%A15=j0|DL)+3vrVt7+BNg zuORJnR}OjjXOFzTviHdz-Glonc1-^~ZU}@6YCv&Xe!R}@)K1@^#JJeR_ZSZsrGjntH}QfV&D9Vhk6A?;R1CtyxdsxyIA9QOaiCnNE_^(|=lMbHaGoM&Y=o$%(A1062kIMPXmJbAq+R zSd`*ZN)e?XhB4ebnamCMd)*!f2|VN8L!|t|wniJOUdkI?JEA;>=>U4G%l!&^7w*OR zhvd`Gp}JP>%&k2$3)h&WShClXH~k;~_p%9uQY^jF%h=&_TBL*zbU6I)obDh_v*H6L z(FdDpIoat0{}*`$ZPMOn@g%GM$8VhV&fDoRWkK!vywzden1=TrqP6+Y22xBu_G`u` zh%WwJwDEG^?-JTnmc^X=5Mv!{liQ+R(`i$+ob596fXnS`_y z(0slG6Kp&rHVUJt<;LS^*-J8pZu>a!oC>7``!%M) ztI-*LptR#NveP)i4fA@A=?DJVr}cb$6-#D+IN--4GA+cBj&yGA;`WVQPY8xzH8t>T zFTLv3|IU4{Fq2~ajnitW=lqc)_39wj|8tJl@q>?#rxGl^;x|LNrL4%)?qYS2Cdm*F zb9PO)E70|}_jezKIQ8u2qs)yX-Qg0A=hXv4SMkZiWjO?W=q?%Hs#de$mH-HAV-15MP@k4!C7n@!KkQbDP+49 zh$mLov|MXT^yy2l(5S;rNaHdth%L2mjJS`rKg{XB-<_WKL$`HFZrmMP;y13ErW^U? zMHos4brS#G>7Qd;7um9gh(Y`^2&yUfZ+ zaUW#-kuHYBPf;EAA!|*_mf?QHXl?Ic?x95z&XkRt!ReGMn{zf}@B0>h_$}jAb@X*V zR&mr;{Ke4FSI3-bZP1*^cO~ zu($^hgj!F#<6)vNd#+=0UGlRk3YA+>~J&uf>zx z#>AVh;c*l;f2Fg^zvFU{`sfkZbM4bD4#-J_oRC^p%S3NF$Iw#_@|cPuN=L-geMqMN z4-1IhNi;PIL0Cw^+|97cS9~l{f2GXbf$vD1xRDR2;Y(P`aQSsgW7S3cX zL)jC!>s!wl){k{NO3DJn@H?I|X1_HpuOvmCP1p50l=wS09WC>n*N;g_{u4`uI+T{Q zyvRU&l2HHtxDpWUC+85N1AAq(E)IHe@j4~nUu&U`3$A^ezk@DFVFCbsK~dEgWJf{( z#rfOs9>LMmLV$3mkw_Eq!%KePNOhfo-$Utsk*`2!t7mTNG5sMSBzuLsiiia!c0SL) zWY+a5k~i}hC@a~`pdbz*IzA6M2_Ba!BJRAg{kmkQO>|O0OIJ&k$w5AG2kH1d6*Nkg zAvd~cdWNkQ7IqgK8UbhuJ3dx+g{ZE!d6%XFAw^E;eW8aPPyh0+hat=nGaBVXm)f~7 zHF+6CKGSSdY{w$Lw6vr_LL!7ndTcQi1br*J?#Eo=h|#Cg@Ds>mOZ&sQno!qudrUQ? z{5xR>GKk3k)YI^O-H2WKt~ti!HpYr40N8#_l5BLl>hm$I#bjo3#Xf!QPZ^W5*G(v; ziTrw$z+2K+#9pKI-;)Z*dO2qQ>a8d?7O28?EEY$Gwy>`Ts)N}tyfC(%l#s4h1ZGqI z{5ql0M%^{8m9CSl@v`c0k_EAE@UOvTa5fA~u_0S5|ehuiYuZ!rPuQ$soi$-$HjzjOTHD9db^p1Ubq(NK`29%EeBs8`H@mc&T97_KXhgiZ*2?}LCP1#>@7b)=h>>+$ zPrZg^7m8BJ?o_X58_Z)m;+hEU;N+Ot+kNRX0vLo`Bj6Yw&0FwPp)~IeikLH%(`O!A z0^k35!Ese;7r%+@D1`(oN9q*&Pn6(!u<9Eq1K5eD6p7NPZuppka5?$HFwooluf*Y0 zDT^d%8w$B~gQz|0hGp}2U?wn(vYBE4Q1-9^^|+APYu}cdtcLSf;=2{g&klLBhmTrg z`;Q5QYVNsgNJ*T&ttPln=T{(rW3l>!|8&0FE3lrOHxFE%F>duIuvD(`eB73IF!UH} zTugPo2zAh|APS&9t?b66Q2VDg_7XHOal1qqbajMq0OsBpGrAt{q9)c+vePX&dJ^pGIlIzXQvh$7UHRGN4!o&8 zJ~-r#MC_cGge+NeR^3-a4J#6x>=)4Zv{iDZR`;A1zdR{!k(~uKCpf>&-lWectcumHP`P@%A2-tX+)4SCv~vLKr#F3@~~e7|cAOBs=}fkyMwg z2Y*(CCu@&5mYmxwzOt_GrV5*DA2k>_q^limzIv7hz?Td|NlnU&+evvFSS4!1Z^J1ea4PR51pOm zGZ(&eUv2C5d+w_1)y7?UYGEIiQ>r!kn4GE;Q%V>jCn%2KwuzQ@kI!V4yd3M=onEo+ zwAd~oM^}8Nnd-+=jx6^j-mD2>6xEMS9yz2LH1I#+^HoG?6&+@YOl2@PV-QLblV};LqyE(J3r4)qE@T8^s7+LpiEcd$S6! z?Ab+a@N)cB!S!~Zdvb_B%~m}dIBj;u$cpp)cJU@`0=1br_uwyd2yAXGK0RH5d7Um# zk(?Z(neh1>pu_}0En|D9Ud!o92iMm9!|gR$OW88ziM^AZlb7Pd1S+!~=$FW(G$q~@ zuQM9obor;1S~~dq>j_S-$&ntZBWN3matJLa5f@iA0mkGBA@7@K{&}ahf8ZS#an4@k zIco#?=0)GWoRDrL7#OTrt0I$~C@3^FHD5KBmuHTqw@u;;{fz3M#a~o^`pDtjzv~t( zu@jpJdZLCt+a3KyovB%Kzf~-Fa6iDmCiU=P4&onk$S7n)e~>}69}Pij%>f@iLZ0?h z5gQOx#nag>N+s|tJg^jazZ!o%2fh1DpCdU{Y>!bz-&W8D{PD1O#;rokbmkp2iW-;J|wx6g>j;Gz*=AKIx&< zkxnQ`V8JPBl74@e5~ve!C*Xh%`@b3*_wYp4(2WU4HKja)a-Q=w>Y-%QmP?nuE5*!S zXjiXh{Zn*qrX9h=iTK90`H}~ld6^_`)`;#t0&$c+aP&0}QMgC?yp6ZK)Xr{7UOeCa}`WTK^oEhjK(pl7QxeZiF~iA zF8J#G_r*@_X?>PEq6O^3d{uV*ns9ChTBzkv@-8x|fOwq((Qm@_zs8=hujG@Ah(Mmk zdHKXL$gYhd9oNLNCGyOr_e4v+B5vPXlRgmAw%I8d)tEbqD8yM^ve^>tf45|`$E}&! z^ZxrTdya7bs5*DOZiTwhhEB(6j*v=h-h}&Mn&b1!);cSnwVR18SGXt`c9rbrblkc5 z^UoALMuVK2%)w+$F7|(Tj}{taeP=-@{k@*{TQTNj@i=7c(Jk@C>rZY1zjlTSSv6(` zqC(?77yl>{>0Wr}OKFGXtGh@V-0)R@Zbap^*sE!J(}s)ZRsYdZG}3P79tToa^I@Jqdvb9HZ69Nyn<+*u#h+S$^e}0=p|V(c@H;W!ELwO zR?30*hqyx;_Zt!&JB9K~?s1$X9m(S4SA6+CE3|OtNWV=BDg7ejjcJZK@pUJ1%2qUA zdud3J)OJLpr;T(>eMNpF8b?hs!|w05y8QQl3wFW-4+i7&4AgqXq+G;KuM=j;=abT(H%X26XnwlVqlc3OK5-yd)dMQm?$6b)A?Uj{_ za{bT{Xz6zQ0*Yg5k_E@HkG+gt?Dcix^ue|)DU}I*6DBMIopLD*i)>&8J61LzVs6~~ zraz+|+V`UNAd!%zmk@Aw5nNzr0AlxW{>u~$E&w4P-H_L}|C@x-^at@!lpFG_wn8d{ z=Xx4m5%64UleKf)QI=;O;-JKi5E}TMeq-@%giS!mxaenzSFLEc@qW7#STS{cHI+_k z@^UmZ;Of8%-EQ@yA9x*154VDX^8mJfdmHIblXE%yt*4Uz71Kc+uJ6FGogO!nEOoKg zG>?wQKsGh)5Ez=!aqr~4P4gZ}Yzg~(>-^)j*-z*0P7|pvR(g%b9}u}<-bnDRG?`oe zkdA0$ijI`viy;>%tdCS>1gk}ufu)c3=ZTR?e6+Z$qB|tn9U!65_%_lOzmj%KX0e;F z6Bg_NXsTfVZEK3uza4L$`w{LagSylai0x4ioH1i%c@M)%Mp7a+;trCU$tB zp7GzPL#6NL-HgvVUUIRT-)paU+1iEn(~mfIQY`Q9?iva&*)25m1<;GYt^u38f@bV| z1iP^jg;F6Ak|0rQPz;1LW`r8HV=ipcQ*(`K?HkT_iEo(Ty6r_U_206-d7V8TE_QzsJ!YnX0g9Sg%UAt2bpT`=TGx zDy_N;xK86=&l>ByTTflmc>it9a|$c=tK>#kItBIJ;G3$w-zj3o(RUJr+Zsl;_RGHB zg+F}wcN?~XvznB+o(bo9FxiPXDIYJrr64yoE@X=T(#5XAC`4;lGTz9RRp zhHs7{P0Y5P>Qa|^3S3YsNj{7xfzrWG6s~1N7A6t@@mTb!6ieH)^nFWVcqZ|l;lB+V zf$jz+EOv9PWr94PjCwwE*uoE@nY27|htW=CEXzr4^77?&6%3sTZD#CFka*rk%V4mT z!wYE+fA{-WewkM87Y!u*yUgWEaEEP%b)o@zf4Re?644?a9UcP4r_|d^YTUs4dJEXT z>W+YL-3~Wx&`}$e>}4hXbClt}G&{C1*=k<<8x}RBE1HH|fEVxF#D|D*x3<+v>f z^={D?=KDUC-*Ph6f+?K4tT5 zI=fm}6kE4^+)75R z4iY7e7F}i(uli4m9~6mRrEBCy{iVl0hch-r6Jwwe!Wgz`^wz@5d3a++7~1Sh9P z{DoK!V-JHTra=gaOXKmy!8&^~7tFltgqYxga!o~!j!DpA*jj$A*MzC&;y;IJYWLLV zj1~fC=8nCNaUVM6Q|Ec_6(&fVkJyqb>O8>#Cz&XlP+r7Q7>L!=UYcLe)tJOZGfOkpW zEQy-dF*?)bObS+3E&X>{7(m}AW9Z$TwqQTEdtSV?-GlkmXgJT+W-8 zt?wo5czWJ6nRpVm%7!y%@5tY?0pdhZveBjCE^yTtfz&L3U^I|V#8xLmEV{|WOm zewPAp@^un6@i4|L4b60!#j?KIw)GB|*_)p_kNqaLI2)J|>hZ;(l$AKvdBTC( z2@g9$Ice!8eiH%6OyP09&sP6astn4RKECw~512?jzJhq`3clvzAQf6G>l|;JFc3Q4_h3d~M0dD}t~#q+@Yxu;w4E8J zT5^dhtBV_+MDKL!CY!4|;PdofCl(67Z&iuJCqNiT8Mg%HA83d+$UBnNhj}=X_mWMQ zaM|4c*JP!rF`Svj?e)F?1v9BbfsffO={ zC~$Xf5^z7)=UGdb6Ss4RHr0)A(PwFb&wkzr1JRjfmgZCg=KJR2kxfvlSec?(?wnSw z;t-KrV8cHIp5xbDBW=6t8RZyCN+m}R2J8hbnu#fccyH%T>8CvWM~P4ap>+(8GebNd z(FDP!_|eG5SOEUgdmEM#s3Lh)a*0Q{^aoF;mdfGm(~e5#LA#E4C^|gFwT!+_&!+XW zp4XOc^1upZqh(V<|Fbl8>pi3E?_J*=E#KkvdFH*wJLEr=B8;GsYb@HuHEz5QPGhdT z!u7E+J!H!MeSlMrj!!ubMS3!*jb^XeAq3vE?Yl8%galuHI4= zpHZQU$~9sYu?>d>#$lGj^bCesaF#K5fo!;~*mU&0%6OO6Wzjytesr39pTr(=P~Z%1$qlC4pYA?JSP|W&C~YQQh>$e@5a*MVUL{yum#r3 za(whDBS+XI(6ulg_-Kq;LOuB*vVjN=WS_d!RQx0gEPi0cb;T&;419XL*V4m(1`Y9+4HBdw%bpN4ZYEc%KY<0l9?l%eU zJ(9lNUyfEcweF#E><*-9`bnj16_OC%@rJlRl*7th(l}e(UkPcx;y)<>zMgx4oi;UA zbg(c9-ZO|rl-n~m*vvrBO0<(NqVX6V)je)ePCHJ;#*z4Wcyim#JaaLsK{TSPEr}PB-ijkL( zx?&$X@SZ&7;=y8URws&hR9JUll8PL=VHy)E@ZK}eqIQXuAo3e%uwA*J?CfQ6r5Xzk z(gO2jj67WH<;LdMiKiETZsfTfDPU5>#Q}?{6PpHfo3hbu9nu5@FFKtS>4*zv5HGl! z{W?75h4TUQTQn^!R~Y7~GC=S!`xR3%M9+kob{;y$l6w$gv@ z(cvlF_jXQXhIr`S5wnc2cJa2YpgBOR@Q2RA>;Kku(0hqN03mdxUc6)`FgR9Si`Y@q4j}tVsXK=2WFFJP_Emf+JuyODKNE@mu}4_GPpgCP(&4mffo9 z$2;oBAQpI0$LE)QzCl!o&B=}SOP&>g$M^kTsHOh+#nrG52O{6$EeBiF14S8Z50_k} z-vy<15a++D;#fBg`Zm>--NUYfGw7?w zs@n=?8wSdUj;S5JG%hh=MEX`zk?M0}va=16;X7ej^L4UXAUU3sgoClTET^DFHlJE| zdJS!@X}I|c{pBp~=BQmv5BHw#39GVWAfJUW+4NwNxV_RGeno&mNF%+q zJq92!uh#T#i-~cF&&=R11IO-UHJ*}>udsk_DYlNs_cH7MeQz@wqmYj7FAC&k{ni`4y*{ryNFobw`1X>Gj~t*V%p(mR({iJ{S4-eQ`_>-~7)F^p)r z&0FCAx^u4Y`7_c`4BAj}on1`3IQUB9o87`=&De$Xt7>EGnx?3?#lr5pL=TyjG6_>t z<1OJH;|;cL(UP}~F>CGtmHkyQ(8r_Zrz70|{WtE{_TxKH-We9w{QUJDps?nPXmoTQE?%y*LAI}(SuiR7h z`7|f=tvYW?fPLJXdpvJur2i}|cC%QxHsI!1hg&b|yDUsd0{S?ZmG zf9hRzrIm;!t zU=-`+8*RB6bKCce&f@0ReD#$}jh}8uYi@C(HTp6-M5lPnHp?mt$Aot|aymph5SXAk zrTuRmhAbzKBZLRq`K7OutlVmCM`@ zYT$7jY(pjyu|99ZD+!0%dal7N50x~;HfaRB$1xjM25pj$9y&T(mO7muB}M1P8(Y8n zsBS!{hQBWMcU0sJs4?Wh0ojcvcDL0;RLL{kkdpcZU@87@G6c-4qVGSy+%SJ|Z}+2^ zETdC{rN?)M?{&5DUG1JmzZB)5`t*dS&5mtiA1T=_XuvA({%4l7W={U-{@3s@XXNp{ z5S0@0F>5pYMCZz3>-K&HpiH33hFspWqDf$eZ{)qumrScS`G)L9HDQH!N(RL7ZIn;K!EcX zkqai{CGP>60mu}|%JV{palIbVh5!hkBl_yksFdN$n%VZl=~ak*E2l`YCot#|>ng79 z+ZBS00B#1L^dM+S!kGZ_1cdyEy5|WjL16`;oC5Puhys%90pR@yC<+PM_j*FGB6!wG z0`54F_dHT_;|sr$vH*W{s+1DMq&NrU1c%aP)ERB{m_^zLhqF$zHvfa$eFVyqK2RNF2dHkjQ`&#)%2gViSN6x>Map>6!HCv!&0L zlfL-EyeUKLk+;4KKL+xGPvpypD7Gw3*HVJ}y*z%AxR}hnQK%Y?Ccz)$?s4{%GAMbM z(xCD0FU6|Ak`P`n@#;kYOhO%^Y)Hs0$dOU*aP9_YE!oilwa-O9-mrY{d%8EBb)vpyyuS?Lrc zo&-%uXf}Q-!YuqRnDh2l9OIGn|F8gN&P@6EU>9rFucAH!siph*ekNo!@loh$gPuCd zuUrNdd&lTV#eo8(lMY~XNF+mdNb8?$t}y?a$R2ujQcZM)<%zu9FGlk}LOYn*gC?B5 zL3r_%MBUrK@iY=|X@Al@mk(LH;mGKs94O-+A2cO=oUcBf@2c^<=ZWmP^E6p`%|EzY zm@D&I;f0`M_GR66^REFtu>L25t?&_ngVX$tMimd5zjU}(J!K5d*!8bzlGU{!z(!U9iBhVAPag1~`ZMqWV9 z^D&+Oj!THWxKbOUbZI~TP^&vV=ag*26ob5SS{2){Tcv6{kR}tC{x(xtY{7cl`bq3W z;0Xp`XUXRDJ;s2h&|DjR|I~X)CtWuRNadUGF*# z`&F>j6kX-GPIKcK_`-Ke=!WX;oB;2A3d^)v~Aqi*-srUM^W)vpvEeeaOtnE~BZuF$DwsV^2c z=5Be%3x%6UvX|~f=~>ryG(G#8rgC+yDe?F9?R=9BoRI;Ja*sVl0@736LPBDJHyr_I zlY5BUz`NrAF)zZ11J=2gqN15}q3LERgfp}X0aIK{QU8!S7A zUIQI9E~x`%e&%k6W}uyW)C7-o!nXFm7e@sFB>?73x%0o(pb6-kxtqgJn8yfe2tZIM zUMUONtIxcZHbEOt6Mf{eRprPTJ7f+&x5K}nzTtr;HojYhUg$jPx&oy4kDMY@^RBV) zs(I49BNxW;9(exRk>9JDoR-v*T3FA#h|TyN>(>$P(HO-3c)Pxx)3i35)CI|XvOUA> z=%%8?iP-=l*YE7=yKi(vpX)@ND)w(e?Am!-$CNmoToP1-Jt_A{P&+Epn!fxl${hEE>cw64lBfhrux6tYg*Zr32GM-|jYD*fRa>WC7NO#+00&Z3v& z7p#%ym(uh>>SJ_h6!6DfrZRJ8w~t$8t<5)OA)A7L4JZ0Pa(fz{MscHV;;J?56`cL zM}(TyG`7A&zTeCCMPnZj$)M_o7pPzg4oq@Z$nQ%c=r^hG4@ai4@Q@Zb9o-eisG9#c zEPEj8E&AuOO6bejWGP@B==7%N=GBjy>Qm|2?~+uCfgePj{H5~*j;%RYOZDB0WK8r#Q)1KdR)V%%afG67T#dXo{P{)+;ejt>!0B3EVn zdSNQ-+sM+++KC9LHWyg*F&R)gSF^J$5`70sgrLky8BCw;v&RjSj~_8 z>C3e%AnEl_QjeM2t}B0P_^cu<)EV`I$X-QMOTFADtn-N1ju9S*?qL3Yxj$em2qX<^ zWW4iMF`xsqnbBs{001R?HM>+h!DDR}82+D6m{L=Ez!ac{%|z-DlFvV)Fr`9sqG#CQ z#_E}IszIlSmiR^w(I0c&)$w~+2RWbW)=S({QlD}QCU#p0 z56}SMI{D(qw%B6gRj9rZ?u6^yi*?}__Bd%|db$C+EL236sYu`3uR6|aI4q1fEG$bx zc&==+;EVy+4|hj-m8<^h5DEW3B=izf10;0tzAdkroQ^7-_73mgd-lW-JJ{Yb3Q?Z` zWC5KkuEMzhqSjar6D4*O@WotouB}A{0Gyf8(2}=X6uIY=A2`c;A$0h3#UiLZe!Zg| zu<)pXI`Kk%MC}5f;+~EJpCpmtYsf0Z0h;twpC2&|yh~Udpic@m2U{g24_KLt7xF;+ zJQq#rsyrgmL}AQ#mmZby6CvtJ3r4l+rOmKa2XMMI=V!0Co9-*5Rp>(2rr6cl1_#i$ zwd^H_3qRcK&?c>(P)T(CS1l#@~GHLIc6L=1v612b=lTPUPA#xdw%b z2kBcU;c5j7V>9;qKAhj#5t+_OgiMVfrTXTS#z9#*arN@C;D(*#N>ZFLBD3JjrVG<@}lx$dIT-OeJ?U*Wu6aoIhpQn$2QLG9eD;*4~_Tj73n*5`T(9uv3ghFw2%mwTbD`jZ*(aJ#Sh$4mgL` zkz{BhvT<*~$Hkg~7HQwiVej7U3AZ?~WFu=EIfVsA}?g z7#uKSxJ3*TPfKt#g~Rxwu9i5ygnKlsgvMmV_9p1{lW|OIgmfL^TIO@|i>czTFXA#x zU{=IMR7OW+d&Xqj*u)cnhK4SMq!YNKJPX$G%cJo=WqL;7#rE6|pZT|KG@}pLb~`F6 zKD56e4h;y#%}W;cTAt`r#%N;w((O=V-V-{_u)@9_*U+cG*eE-VNrQ7r;jcQ>t4(eY z^>;Ikij3V4XJT{CJ`q=opz5Q8 zo#~bjH{dA__(BPWdp(uUrsG~^CVHT^6MRUVtX|*Rcl@8jBxT|1?;D(VUhS#+Ju3%= z6cIm^L#hbL>eDNz&)sNZ*AxD(UN;sJEfFpq1HTJcotV-0xQxQ7&UcffGU()HxYjs& zBm%}veD4GcKSVwab-Woh^1;opduFPK>#g+XjSi3zgW8Z@i7RZtjR;N%S3%OpeJRw@ zJgiwQQDyk+w&)1DJc+-@@O)^YZ-$o*Xi?9OWTJ`vJdS|4%l?x-a3%QeGZA>^oB!Yl zf=viPh_rGkQg6tMUx4`ck4Nk6FM(MY?=5-9$Ldy87IT8!a#*T&nlFkxWB+778L{^7 zQt5m-DD0oVoFBR!8K2~|A=Z2)kaOel^bt}QE43y8+oK%6A}~2r6s~=`qJ-?PAye1p zPElQu{Z!x+c4ilc|4mZ-UB_D)!Dl9r*XAvrvB!23h%uPt>Qe?`=aSWZI3Z{V58M^Y zf9?4?{nRgD;P~~!D&^Rw{&IouM-Yjd_)S_~Fp!dgvlb!O)8lS}oF+M6OIvJD}#y7yX6 zv&64Dx=#a~$K(b@K|#j-UUw|lfLJjVQ0NBh=HV9hA!0=@KHhq5ORDCdPgc1A&mz$x z8_0eKRCD!~dH7xPtfY%dpE$isoM2eTQo~>FBa6>~{ixI1zel|UnWPi?+e&w5-Sukw z^2=DC6nK6P!lJ1zPXdPu)rnU9g@F6(T#&E;2Ss1wR2b|re~NvjdPVgBc$+BG=`bo> z3Z(hVC8-;|meHshbZUMnvfyYaRLlvOnhIZ6;>S9ksJf1b2);4SC#{*P(qDSl2`b_6 zKX7dR&{ni1BfYn1QBzdu>1X_wD&>sw*%) zk$p;9=NyzQY>s0Q|2tq3$&|H7(UC)LyGgj&O3_6q^pJ5q3in*n&8_%Sn_cwI>wGMH zMIssp0p<`fnUzy(v|$p=pwRjSbuUyjLOVvU3~G}+7SJN7_=sG z3ti~_W-$|%SPKt%itgq{iLts(_)>2JT1#up_V82Y1`&2go0ozXzBI*B! z1@NCP**9y5HkQk9o|#(feKf_BrJH%AmZRT38}>9ae?i?=I%wfbb|rexVfRz+%Al?h z(`C18&CbYfipebtQkhcn< zZ5j23L!;BJP3wcM=cB#bvYM%ROPqMT`Z)|RX#ThpfT6v|+r71Rr(JMe#4MnU7@+v6$kJWP?NrRc)B`>o?m+o~hg-a%Hm&M{!fu2d{^{XzGa@)r7I%L`i z&=#{fNG611RhZ6&ZTMo?O-v zpBlX_JJV9|1N)hSzoffp(hlk~$kxPIt%q;3znjK;Pj39N`UiG%_~*d+KSXbcMkR1- z8!`xCl<@nmV9+xi2z2#jPQq`YFpbM3i-_x@sqs)gZ^~|y&lAM)$yE~Ek@k4+THQ_l zXy1Rdr}f+yB@IQ6(yq1Ur>nB^|)aEp2b|e z)I;*zBv^k<6V7kfw&oFg?Lz?3xP&pggymMwK04%nY5!{R7#(130y9I`+=A7{k(O=># zgWA6NW%Wd+sXoU_v?}4d1h@}}ofhr)R*x=jzaAeVY%k-R^QB+R-KZC>&4X>JB_d_~-`OYIe>O7yYvBL8483YjTlnKhS<2n9 z1G<5TuJHe%7m`R+;4RN}Yv2>P?XH4uUeUj&_;-F)1 z-2$Efd6z`nu5D7qHSaDeQ8uxlt)>fu107K<7~qC}=J7i^!&2sj)`V&GJYj_D{`%Yu z;yHX2*I__Qd5bxDJJHfxJ7KOmFmaBDhcv0jR065kaPsmfkn$btu?TdDU+}RCJDr$j zSX@$2c#-~$z~ijwfI8@2FwC^KH89NQ_R9>shMg8r)u}#X^V_FiN!|~|@hu?~>LAlv z^p~e4r-19$UR@rkP(O+rB<-7|*zr&1sJhsn3v=lQNo}tl0T-Rxv`QrZRwx;5}akVcUGaWI<=y7UhE-G8LP6>Y{$QXh@uh`8Aq zM$GZC8zR^i8q6nN=zK8wB*=|kxY+per^(cewelifD1VfH4d_zF-;U78nkN)f3nKd6 z(Zbl#Qsl)IKA@zDx|<6cQzEy}p6CTWX90(;N{E4F$*@}A_jSv$?$!oQUhY{NzlhbT_`nE_;gha*$+ha zpe5D!@qvS38Y*UIMOI{H(j7mih3%OPFXe7Yu1;_9cs;ymV`h7)ec>_L)7^JlH&15I zElF==?^!-lz&3z)UtNv}zJh*^E)CpZ++2L=qWrj6-|=dViDJ8E$l$WrUA)zT$dm?F z!M6!>pPy^l75lC~5AY$IAAg_i7j(>gdPFCV_+)>d50v*GKauU@ba?OjdUh-y@L1`8 zWw+`XpO$`nkJ(Ewn;x%uXFUw!+fp zifS~5!REI1gjPa9*Kqp0r|WT);l3~9KFa@cM8I679B7)_W^kpzKcTD?EI}XeHjU@c z6V=-!U8!>7uMZ^I*D0zF!d#S84=m%w3V>a{#c?6I=rPfMbDm}k^(zFudX9*9Id+Vt zUH>IdCC^h+iD?(q=z3M>tlMSl&hROS!SwBKIPQ=k9XdJw#0shJ*#{~$`DTogjT@K) zd9Ut>P52d*fQfA{JdkvdDe|PyNs2YjWwl#i>pUM*nU>N3lj@_(K&R~+ShU856GEKVjUz{v5COup*-^D$(Wfv zEwekSVcJ~2WE`uj@y-+rPlzi<75N9LeX_xZ!$d48YwVbKBI_mY@`P#GGM7MR?dxb? z@P3u$!!O4n6Av>U0D6vf#0m*c1i%a=JU{Ra;2___0;T#26l)0Tm^Az>wcN;p4cGY z4|iHPie7DVD5|uK&C|X=et2U}G`AoFJsYx(pX~g#`!xgs+;x(RW&YyiRACV`O+U* ztmtZmW_}l|VqP4>K-UL^T-Epb<}qTZo9{ocOAXa4Y)KaUQri}3sM@huJ@+$tXYepE zA88D)$=Cn%sego-1$;Viru0}d)4Z$P)z>nIebPG3N1=ktNuqDHTBnK0_^!$*EV@3x zf&Q-tCi>i;aFiVie<;!N0q)!j+nK{(W}Ibz%{XI6m5rCIS7e8VrH>A(bX7TH#2hnW zUtkH@{}bXnj~U5`wi4I{-Ap8WP&iXbUk$VH`(T|@5+7@(@^)0qf_(m)!q=@EfQQf& z`U?r3gB4`(>(kt8Mdxn1EF^(`x$$Qi?(W+ODm2+E9K&>e_tD!hVaL(H1P7WuS8=D{ zEpXu6cH_YVB`I8fKYH*|epjRyzg z#V7SUCaJt1)VpQ0u5;ghN{OU;U}x~wSCt9}%OB%<`d;ERXA2Cibs-JiN9jrKnjn#5 zg4B^!r5g>brkpo?BUN8k9GoQ%qQM4~bG9PTQkM}lnd>lWS6P^L(dh zVloeKPY#TSG^Kjh-29ZU|(UhTa?IrUY)+om|!2NN>PT5u_c@@7Q2JbBo z&sE&c!&BcHW(?`z^IsvC93F+&^L5|!Kbe^y7E}#lT0Xq%dc2z(*FUvUPUnGUy|}@k3@;gs7rWd z;pZr!81_fazHG)Hxm2VzWSA=}s{(d+6-h_luq|% z@--QW&hiVNPRC$1dU&SSqQxZ6m4kHC(x1Jg`=|b7+{BkoUU4o=!=;tEtB_)vlc{4y zP)9fifO(&aHLz7jzfaE+!Ok7mAFRdO{!t$kDz$_g;g+<;O z9pYvn)S|Ytr9a%i%`*Sl8`BJ^yBYwT@WT&fMj!BkvNR%uw%L@0X89DnsYJG!<+oH^ zGxZf7yRr3Bk6QV$h!T^#T-~65lKWbalKPz214rJ_k7_-Dj#ge*qIdMO6eDG9+$YiS zHUhd!jC(K-PCi1KY|P_fxBW~<&77C9o41moACX_e0&8Gf@6-PhRwLd&#`mf3qhNb?T`r`ybtlWZzPCc^llSXMU&nqD^;&ye~~6YL2ON_Kc} zt>tRZWE;lnx?k`MmX~3*v;AANoyoi^ypz^ZV+yI4^5ab$tQ50;B}KsC$+gzkO0=XJ z%LJ?IDNQ0M5$mn)@>12o_H$;b*YP5*t0K!<)=5*_7Cd%!I_7=i1qLh^xR2-YJCo$# z!hdUYDqodtDhF3$#9ZyfhG8K;!uOr)Rzl zh;ow57gJCvI4h>2AqnAYiL-p63Ga03A#gR4YDb+@y#}(Kx-R;4X|bM;omFU20Nf^fH6+UD!~o$plc;|Y-rng*{2{-Wm`fzRS5>BsHXZ3 z|Be*)`kc=bZ_?we{H7j)_7p`M%6HC@g^YO-$mi%%`>WE4$@;9#TINp4@jaHSGCe-t zFGd{~Vbw7k5j}I|i-td~d5eE0`%@l`0Ca@X zOzY4~Mxt;jXHG(h)0u%qqYi9-rDQU!rjST(x$5}?;e4^?Wr|Saw&*j5M(jDg$8J2w z{rH;p{8)H};q#n31yrJOeoBy;3J%++K8yHQN3De?2o&#Tj?g-aBsWB8lc3)9-58b7GOqJfTnW@UEWMMu2R~A-G9o;Ec?609L zm4lVzO$w(OPWxWL>PxvJG-Fr~wUa3cCj&p)f3=ON0^nAI644nk>jhO7x+>vmKa24~ z-8m9(oD^*t_?x1vzB>+jw5(=O#spiwfU_KQ(k5)T{4@9-q zol=jqV;OHgn_*z58va>J)H{XvDgG;aSEDH)_eV*9QN(}T@1@=MxZu4Tk4f#x&->Ef zYLiun|93~-6aJSufrFXEAUXSi+E1m=&8m7<@`h%8E;ntmg14(!w&pTZkdKwKm8j>@ zK|=-K>`+2H1g=~dZGuA|TXOddrYQ(_+?T-CN{ty5rOkvRoeSV?Q@<)bdgNMEVbVE` zxaHB~`sz|<0qvKMZj!ASOB|B3v2>fY3mrczfpep?R<`@JbAyT$CYO3%x=bt5jV*bC z_X%?;dkfYYsa&D%Atf%^f~rq`N_QL`uodu{1Hyp2Qt_3DQ74o51zXVAX=9XF1+A+Pr@tLr`g2KcTR zGa6hb-rz4oU!Oj|UBEjeyQV^l7E%~e+O;Q?*H)NtG|zSO1i@)?muBdr@C0F13Q{fX zAK$+En)%`2?nNE;3ss@M=dUahq%)XC^P-~Ozfvts{Eb`EkU{OPyhhl_H<3KjoxJwk z2>kz;`pU2-|F~Zf34wu#q@7~%(r<*js7cQPcpSevRw|Pi z0|9^J^~p(7{#@!3Bfne}=)dB++O2>0NnPC|llx=a$FyoFr8wO?)x#{TZ*|Ol^cwDo z#6@2fcn>M8J`!Xky?>k1^=}hx$=GBPH1cu;x0cA~$7}Nu!YC^ZU%xzWOSjZM<>K3;aL<@#JjqEEr5(!7ChTPeQ}mJdI4w-kcZQd}r8@uR#_Qc ztaesbxxhqA?yQ3R%^m&YT!zj;42AD#zg~WK`VIZE;hP-+1?18K`Z>l~@|e5Fm9xm}j`zd4 zuwj8myUT+6tMqlQI!+-i%c&ytJ8D&b<(b6c0id6WvwKvXDZ{g4 zbx_qFezDVIqjA|zv8WHv+E(kj@b11mV2!ci$M(J%e4jWWcJKvdTs#?1pRN+bnmx<) z?dCW5yiug(Xwc`kK{?ITE%B2c_AXqGLf1<9C#6qgn!!(gOPl&j;9+`4il_YKAU_TEN{!94LA>WV1j0V6U$^PwkAt&HNRpGQ`9Ut7t9)vSkVgiiWSJ7h zAv>9;CTH}gf}?`hGxKFfh8f5flF{>nAwC}r9*(!_b2bLbwtx_c6YR#eTKr4oTNgYV z$#=5&<*~}*x+rBi?Ivkc_QmV@2ZyJZrc- zE~-IHFyt^vey-Gp<)XFdozc&d5;nTJ#DVU{&bbt}n)%QlappTjf*{CM5D3{uuae-5Kr73)q*uHKRpuhac+w7Y)(XvY? z!<%h90q$!jf(k(~uESa6PvPe)%?#t_SvI!A5!{|{Z>XZK$-J@jcyVXIZ6-4IjMSM@ zak^weCu9xg;T(JXnLESmPAUC<>^{E(1+sz8j#jV}pphKdDu??pLGVV-WMrTJdrzKB|(s z1$AWjAorD5{@I>kF_;~_IZJX~?{bCrsqD1|ffyTf4>e0xD0iF!a!aIwQ!a6tlKQ3c z0tEj`skHdYhBkf4n3be*Qr|K2Tpow1L z6t=AiKX6I=WII5e>yJCAO|(X11R$KZv8~kfIZG zO6I%QBj+n^ZeK}nY@n|0J=V3^t!Z1g zFbozZpaBq_s!ZtUk>rAM(+z?SZd1aahp|k#seQk3>v31?7!bV`ng}Zr+IiAOOgr; z?mZ4JnD{!jR_zcS9c=U%ep;SXP`$hBqO^RFBeHo^!`^f|A(<7qIm?G}CQ32jl?is_ zo)wciznx2JTIv6=PK1n@#HCwZ>WcuRp5vVHMXWqgjtY~jx(~-oF9@SrF@u$@3v(YP z{sBu123aqq?UNXN)^!QwYY-r&kDwb+SR534?brVi1HXKGKg<6mI_AZ&XJfp5BB<|I za(j-d5VR`anai?Z1`AcV3c(HA;|Kpf20Brnj}HZe$2xwvHlK@mD@1Z8%XKYVQid^3xZD9I5mx5{4MaL(GW-u|om{`lg*b?V42x+>JY@*U2 zYt9hH*Cl0OA^YN-mHDszGUgL$+25SZiWjWwG0?f828N#8O#rMD;Jg`Z;V_iKd!mpP zfh?ytv^(D~*$uGUOXb;Zx#hhw@CDqC_VD>2)`E?2*+KrRqE#jmsb_S@2EKH+AYeDR zmgknnQ4O3ze@ys*5w|*nf|j4D+F$*7HS`5!Gf{(vOW&ob9g_+A=FGhN>JwQcX)zaN z65aOUf3W}+*?kWnb93v)U_OpNZ+CfTb^EhdQY>?w)mY-#Bs;UYI$rz&Vv~T56howx zceaw*jWj|~tJ!$zyaU$@1`*W*`D-ed8IfslFxJl~F#e#4%Ss%4Ze3I~eIIOfmYmG> zV6!V1&td1VLuUC)#Rwo;G~sv|un4luJa6~pbO*%qeT-h_6?mGmvzw<}yJ;^71N%^;*Y>QgXwMt)=78cFS$Or{}Mdmc|L!OP)Uupza3qvg*AH$ zq(*=V0F=9bBP0B`XRT8>=5K|$-e14QtJ0Ewg2*x+jD>P3$j;k^c_c%3nnSsxRfkX% zj!AGY7G{3S-fXpAtHtVvYT60Ze<&7~1k6p^LRK~k<9BjlJ}g&Nvys>br}Xkf`@sKn zfbZ$Cp?#?rLp-ol*wPjQsF~67B%sFfC84?4>NchE*A1M1PNn=}-MWn%C-r_^_Yi); zo>sg2an7b+Ex#J2(mT4q5tX15o1hA%b-d;_bOUBrN$}C$TEOt zW>#sm{c0K(k=S-_Md#bc-4IA~!DnDrzKDXHJ=*{*&po0?B^U*jm`pjz-Z8fU=sgy3 z1&me=gSwQOzG)8Ow%$cyv|qQk6Xq@bo#$FnkrEP84t|hI(Pb68;zo>Hb;v`T0>pwY zZAn@z2;-u@!DS_ds{iy=RZcOc@KnVSNJ@~nJ-+;>ILot5+VsSapa(pccJ!!izVdjl z9h*{)d|L3DphZM&*LHZztp65F&>ghjVpi-crQ57^`e$zT!)4>OwDHKunxkUFh!>C2 z=Ii0Dkugkze7~^p`u8Q|0*fhvrWz5#`#;M1N*e}WyeTWmQ*!5?IZv5CTb{?d{IyK2 z-(;@pca>MfTBamgAbAgKyP9_-V;OrLe%I;``@8wRHz&#MJVwY=czwIT($1n|?vbkA z-OTT6PLSG4x=e>F$Sn2DUwUlrv-rzOKUUF2#ru^Bx|Ky=ca6Ix+ttf#4&OL%78`vl*4zJ3vLEo;h2o5lO@)ye6Vo#N*oiAmG;x>y_uuEn zALKYChzuT9I(Uf7Qu@3#2R^K=5MWHBR^#J7lAOEf_4D7}%rV$yV}L-Po!{poEk%@N zNDh(qrH-&v)iKSB>QRg`4%@T5XZN1{PW3eHF%In}ojBfKxhgx8`t5Rzjn%dA9Yl5B zP=OLtlc{IGvTk@0N>AzdVJSvX%jgL+u`>fA5+7jDn_+}xz7Pst<~OZ`&Y>DSgLz)1 zdHZ0ZT-U?6TD{S)Grw=qtn#OurR7Zj9zkG<(eekz8 z?a4I3AtzW7w%-2o%64jOc~Z|TptWf>59CKwv^si;G}Sr{9d-pU)y4*H2ujJnIhIlu z`+`$D1bY4y+|et38c%ZW^-HaYXc6Fnq~WH3f2)nzmO>w#)-+W`(M&qd9W2JQp?UWb zg~9J_^%(m(ZU^-0&RXlj?HyaPg%&y+sg*Kd{VMBliur2HQrCRwt+EWf5EUF}eF)Q! z7W5kao7l0LNWTaDBS0g}Vk5dUlr@y(C2IA#;J)*@*P7ng z=|i_8=kfLx6?HfP9rJf!(n?u`RrBQ_1dn$kC9*H!O8$nN&{a%umfIz2`M8zIU%A%$ znAU#a@pkgV*;}cN<$RQe#mO)O_46kZS5HD&F(zXG(fn_PiUBRpY-?+~-;ze9jQn^$ zJEuh*nzuT(RnLaVA1eHk*-sL^G+0&!3Y9MfyiV$NQPTj3@a-aD- zEC1hGa!*MIk@p zr8tuwoF#o8Qq5tfu|EoVc2g}VuN5d0uMtoNWTu2y(TcA_#ZemGbwOn&YG@6TUukE5 z7B%v9Wr5$vJMMZkS#pXycx8LG8*DUuJ=2%GyfW-1J&IR+-;oW^TV+TQe(yYSuCli9 z8$V2de2NP8_L_mT_XFvtX9}_i7HJ>8S0l$Leya>Nv~mzu8I0;sKwGc5+IofGxz z!oK+w&70FrxK@s9&nnYxprO#Q6QWc*1Rp4JiB(oBbR(mWtxznTdpi1+wt$ zOq&d;s;CP0#lg4rWO1YDgyVD_gw7BK@ATm_xWRYR8eyFkiZw-1CR1Mm*ND>ySK z&grF-p4}6-emR{ba_fK^%nGB-_LZUiNAKM@Z?ann>#ICZbk%uDnu~wUu;6`Epx0LB z=6-^yl;g9cR~YSc9~3xE`b)^f9kQ%4i$#REzw1Vmm)c{nM;4 zsugW5%xt^_WE$%S@bCpHYdlTA!?q3;W#*!y9Kz{0*6Op3AJ82a?Z-WKYys?vv(-Ve zO;ssGFB9fm+PwzsJ6_(y4(L0m6AW3#5`}|pQ_dP!U2|3ub+309wRGE*@*h=3;o7DL zFia_;g3)6CNF+0_+xphU{oBAZPDiutvtKklBNab9UcQaQ-1zsx%*t+|e>!L#sali> z$f_IV_C1wd<^MB4w?{VWd!*Dw;cD=NY)83tu^m3`*l)wARu)OP&eiB^NFi5}4?CyI zE8lu9rSy?>ZR0I)LZ1zd>uWi2-o&Bx16WOU6i|)_10nAp@C8!hJ>$ z>`_4FIm_K0At1mfencg|#sy<0j#7xDC3zW#sH+oMgc&fy;50N8+gJL+eC#1#UR(S+ z&&o88Ksm}YhFm}BpB=4Eyo`L z;Jj$H6CE2R(>;fqH|aJuy>2v9|9pS~!bW6t`d5LIo}+o`;_P+skFl!egHZw{OF&^H6mtm^>h*gZtl9r&Kg+lbvs;N|gRr zcNE>=rjoR?!V(#GP63R^k8zecg?z=wOAZd)5|8%K}-W2 zU28<7{%ro)99aTrx13J=-AA<=mj!dp?wd_II@r?e@XF~i_cUeVaA+(LP#kkH(X3}! zDW?Gn4=qm#`DRE1ApjI#YxP?0P_r=+!RAwIJ~ku=9|&mgYr-}rYQ@YT3;c$%H-p-p zClXp{=9178LiysIccJdgZ){T=iX9nd4_+X zh0566vv#v8Cdj3GV4tM zrRN-dQg`7wx|Y&FJHSHBU1dl@8i&r@U;8)JX#%jv0Upc#%LFgWJm5Fkm%(0tGAsIQ z%1wMb+Zslvgi!@__KEErCp{M9hcfpiwVI1W3U1zrL!X0mV=H$$`;7}_9h;G=%nPwI z>h{lG&st_l4jcZMNEC1_3d8-wJbAA)UF_U#0X95$&)14ZD=SI8eK5=5O)KEPcZ4D- zshm&P9Fo}(b7Yb29rRd?47G>iM{FTm1b^fNch7{brZWe99(S6XEh~03uESDZT`r9C zdQOFLev)(s*|ERX=W$?2*+?jfnNp{VCN z(c_vda`s7hY>yo!g+bphL8+6&jcz@zEN4hkY>zWH1;b^vr)ZH$T}Qzd^8IQxAtvZ)VyD5@mTcKk27#^3e%t-&vWJa7IN3jq2Lf62sfK1I;(&=_c_>Ifosw$;Ql zO5PhbK5r2fT69YaRw#U3G?A1Qcg=JCYi9te#tZv6LOQ_sYxbPAfxa}Lxr|}lN%joW zJb1r+$lsP?E%fj3hx3qg@I{2t4%V2q7u`D=ROe+(Dr?W>yj2h7%27^xUKxrOx%atk zD_?3eVAuRK9s{U2`n8V!S=r-r9s~JKT4qWuyrV+g>UvP?`9uL$CGKD?*Q-)H0U>eA z0pcimlKX%{gxAAh6Me4tK;;h-E_Sii#4bEr{7mtb^ZWnC;hn+s2u6$}uiuwGSC?tTT@z34j* z66FYPwRm~I$GKR=@E&aI%!GY$C9eCW*_2jmcqMy?Qs3H( zXoj()fAmLrHLt`wmx6!o%*dyW+d~82ca#C2@et$51zL_}Tu(JrUZ`Qom2+gRa=5wb z-28*NU;f*=kZ!qD8O9BSgYc0C>zP$ggQlL2nQkXrcF>KZ&4x~^Jb4Uz4w@>1&l9%V zMP{1T#OP<{L05-rUnxFTq^bOL_bv!Vf9s+<7;-0N&_udrao$iKQzjh)iqP8gB5Hnv znz!p4+KAc?(j88`>ejI@ZXR%~#e=toir7;BUf=(&fE<9!oaO}Qm6K_<8t0&%Lv58h z476ruR8xD`!FB^P-<67*NN={m8;N^{O5|nVfn!Kky&GL?D`bMkbmwr6w zR0^>{iqoQOZv3N|eZX&@wP(I|Z#kP6V1;v!iQFXEz1-vK;F-nO^43=E9cq?=-h32w z6D4Gl@aFUTcJdJ4En|S(4K~8E!G0y?J$|-hS{i|}NeA0sx%!p@E=JN8sz}r3_meY{ zGqN{kyPQqAJ2&tq$w1L+Xq=O$4eY7E9|+SEZA-YWip0&t-fy(0DcxEpopu=Ro-@zW z3VFgjJ^HqwcH_8d)QG%05AV4sVsoLN-(ak`kA9EYq9vpMkP_Rk{6 zh0bVG1p^T~3=)tmFa(9XzHSsB#|B4c8thc>ZbW5>J|+QqdGLhYao^4V6%sv?Hnqn~ z8V|}dh9*S@G8JvAWce}id-x>mAEi#c9|qKE;Ig<#gc3vswu{wGnRpRYOnAXHsX%b- zcM=>T=HsI+Zedt$3`MhO+)0w2UlmTddW$o+j%9YR@l;F_UgtG&+YIR%+G$}rL4XZ1 zd%6lecR@0m1d5S-7GW|4NBmDD%Vm!xBQx%eO<|KY&kGSN2j5Go9`&(}cf?!Lg+a@T zn6qJ{iAEW-!5lG&s&P)i7`-7F4$*8@d~lCdp@_hGSZ~CxP48H;oKtoO_0QQiccwQ& ztG78T9c|;Z?3~7*jS0H4a~v-G z2d1HX;BWJGiwSKeO5=(h)0W&y79DO`X4cc6QP|ZR)Pd`}$fBO9CjDGzfFb91y?UjU zVc>;OR%FDMzX^BpeTz>@IgcB#FW23$ItK5Z%4Gye*pnQ!gmS^3Ak!_2bf2=?Odb$N zv^!uXe&xBvvN&dc53Ve(Zb)Vj#>7>aR{6RGmJP2o3U}CQTME@eEulCo&Z`+goN=SnIefl2-s2VLw)S3`D*tBb z)x3@d^ZKD0lq3zpEw(A#o~n)+qik1E>yPy_4t5|yHeT@FyRzJ=t~5Hs#8d+d9t6A7WqiDJ@HF#j|AP9`W#b<(j=C9}koB zA7bRE1#Q2N&hR}*UPiAbXNY>|DZw_^)2e@!3iO@=%f568TL_bnEhZee@1>x3jGlsb zHosO!D0+~)xDYADQj+Cu-=u0tANe$wf61*r4rqRd3ifOyYbSC5C&utXZ)|JtB2OTH z+d)Lu_k@9qI)%WRVw2Qj%!A8*Y2QgleVf(a+_rxtW&X%V@f{|W2UC5plY${zqFAA+ zU6H1$#Z}u-I}x25mhNQ31Wd5er{GYJu>TN#c->6-xl^F{(`j#nBE3xgc5T2&; zvF{9K7GB-6?ryypqK(h4wM?0$Q)L*Z!CCXY4yP5ie8W@U-WTA9Nk_^=9J%p;bozS@;Z8BEzJ&ghDG|5F0KiBytxIIxi2 z$L#m4HbTtJKBPw~%w8Fx@TrZ}%_Nlz{7=y6Xj#JmOIR_CC}PY^oO!@mgcc>o{B=VBx4xUb#?k^Qpab4XJBb; zM6oBFZ#ObCk8AFk__liq2^X6W>Lgj*gBQuuL2Ri1&r-M}cC>L4$2r&tXTx;H#j`?G zLQ`brgL?YNaa^DQ!Za>PxPwUS8_U(EhS#N2?1LG{QCiUQd|-ykB(`sS>m$UXjomGS ziGN1*Xl(sGH;_%dA}59*!$5cTI@E!ChURq+k~!zN4pa(V=gB<8t~xOEYh)5 zxu_cQAK(HRKZ zAgZI9gO?k4%(wf1;|=DSU()Pkv_wIlGJR^VSMP0iXC)JXpx?Z97wi?9x0XFZA-0(r zgg{3tzksACEusz74NGJdVxav#I7ogO7o@tN|8aq9eBT-=bL zP|6qF?=L5=e^uVPT_0+?IelJKf{f!i9`zDV0$Wy@YBQ1gi?)r+W=IljA;G4{x{T1f z9?x5oPeM(Jb3>FVqLjfli^w8?(OCXZ=W8Zdhs2&vM5`uE#hCe`Ax_7O;IZgJzfnyg z4@_Gs=>`Ag_a2zJFeh96N`n9)MW^371AV_j1ke&tc#VJK6@--H7-W z;7?I|hg}~j41~_ps65+w7N1VqM#;r!I;IC=zG%k0M;CoaF;6yVmsWq7Uu;vz*Dqu9 zA!b-GGXvSCkgWvzS?yVIupP9}_f^lA&ej$ewXWR!N7Aq0fck8oHWeM!u#BIM`UXzf zMzK90bD?t|#Lb+q#8XE|MlIU>V3p9TwjuLV>a$NTQ+qgpaGMcZBE&&dBw!3Zj1uuT zE`1R&WcXReTK&o&zm6KvKW zEN9KQ+B&Vo2{p_lH&z(m5HtuZf05UKV>p(wa~t0|>K|Geq<8m=JpbRdl*Di*!B@|t zLgOw%E1>W5xLG|2qeAJ;WLmF^G))e0I2hy3;*a8vyb6?*GAG%5=G)^Bxi#P%`7%~ zSfH8|q*n5C8p%anE!bOIP9*z1-DD8%pL36*3Hj103i=0i`S&wCfD)4-UGv(BH=`zs zhHS1KzUJH+H+!di=#AFlVt~!p#VA^`6y6bR6aJ%YYzmO5{qudma1-!Sh4lP+ z=*n`@&X3Q}h=>Y)Oy#%0Yn{4sp~mXBYyLtR9}h?YyThV zb@)QU<-eH|#g;CxI}DW07`)^jJu5FHIal$W^!B2*#ZLtx1WSzYer2UtI??-9SM5zZvj`EkVz3rU|YyRTW!>>5$)Y|E}qxJTK+QY ziY6$8ge|N*f7`9x$~%97$XY30h3xVjhI#(X@484zD)5!`6BntY?pI?5bvqu2s~*`H z3|GaD=tL2=vwrHoe98Uus8uGLGMxSBBgqZAIhmL)r=0XUNsrszYg&C-lA<MJBSFc zr+u1v+@wAy99N2^(>E>i_low?Y*z5ln`>_O&9djAe7k&RT9qop` zs2)3*3w`%mb|0+)d72nnBwnt0liFEhCEKXbN}v97prw<@Gz0bQ-6z9!ogYsn-nZco zJk`?85pd~4GDPz{?vE?0RzFT2P`kugNcSa_wj+ipVQmm->c zW{ahpi>-gB#8x8OjW<8GbG?tn3I;DWniaL07Pz20 zm*ZMKb1GX+c0dPJBoU&n!?VZNzU389J_~HhF)HeZnKGT0@WM`NH|IzpjSsD4S2EoW zTu9|DJNLU+FfX<^(bEX?>~7F$>3Ui}4}eUK-fcF%H%tBNasjSD+5ct{jF1f|sGd&5 zoo?H+bvO>=nHewfSog>t0P>EqUmtt0y6o6?0CF;OmL8M6AIW`7ka@g?eJ---m3upp zOxy(NpG!(;<1|Y@Ursy^;%F;^H}f0Z#@#239wdS_rBnNpJ<}tWr|zCR7JMW|5^PJ& z!YRa`e&`Jw_L6iL8j&&Fh@9gd#vF_S|A&$Co$KJB!5qC=#qM5%C=D>-jvPRl3%j`? z`j)n>p{rZv0&jc{gqN3_dYMlrFw0V0LgGD{_F0Vcwb+t~*fcz2rlOPiF5nEhAC|7nO zfiK+@Iv->kW4_UaefHyu+Lk^I85y+^tg?!p&r`6y$k0#QeVy7>h$)gEj?1t&7h9Iy z%@BJJuf;=n!Gcdu(8=_trA*mnQH#=5cHs+8KscA8tKa{=mkpdz=rxSSP;x|>7!ox} zb&g%-_LR~@h+nEO=C(4*o+^&omY+$@>w2HgXG3IVVqWCvLm~+lu&cIza-L=Pwi?rR z5I0tNwpM71uLEw_Y{r_6`<#^j_SlrgKt^wzP)0*f(m9rp5hE18oO9kw&@Y<$zB^L1 zar6T*P##0_Q{@k(oLge{%Ik|rGGZ&tn?IahA~!*6n>$c!vXArIW>f<@jmPLQFz!^z@Im@bJ%|sn6 zI-86B`=@Vn!eYfE+GCWt^Wuknj>XV%)*nIi0CxAJ-d4?~{^P5x{Z{H&PSOOIBz1NY zX+<^NsNL#W9MtNIx*Zj*7|Y4~g171pR1MOuE>E1){}lJr`o0gu31BM&wDE*!iy_ zXglrxzF6tfTi)tV=xok8gZ|q3>!CrJJXPgxkYmMI4OEHVmE`JZxDvFDK0G(!)vW4J zPXdkoTK5(u&LI<%5D*C{6rH!6)pGppmK`H9rv7B8{c&Y9;~EWF1LGEPg8Q=c-nbsU zy}>s=0k)2^motx9gyWB?Btu?2pT()rq>UW$r=L#bjCcd1atw$W|3fcxSJh@4S*@C# zv)}UBh9M##g`2_rt!JEXg7q4wSyxXu%*Cn?)35e9u>MPp{!)WG8jEfF zx7D+oH<35`2T@oh3HTSg&sF{1PVwW|(3Dju8<1Zap_&Cq%=X{td=zUcg_x+;!}O6f zya#V`D~JZz5DdMl=cd;m_+VL+O$Ka!nu%W506w#@NEsXZ?$tiO(}=DPD-8_3!f`Hj&Ok0hl1Lo-Y+2zEHs;7`Ebz#3E zP%Sml6`Ci5fm{PF&pIQxpJS)UMq(59y;D&h zvi|)AA@}%Jj}Ast7Ep5L#+Y)5cI5jYf3P$BNb9jqdIT!x=})&T1?`*W`1g;796~JS z9+l+Fz*9e{ZYcI&C0}~yS#HtWIp8I^=d-_}q_-2WsDw3}Vzk|WMcgt>*+641FGIa{ zanq)faSE64fRlV0ZE;^SJE=`a-s1z9h{k%cP|U9Vpv_%$;@|DaLdNKC`iIiwKUS_I z9UAJlGa%2k7#UE?{aGiHA@$ch6ZUhNRZBXs^gOd#&KZF6S1OrLzhC-xS}9>C+#SVL z{;IS0>nF)7Syv8nyp5O8DFd7+bz)sTLqvSM#L4I7Xcp`RbP!hl^_%_XNw>(aLK7OA zGEUY;yXme}YBU!-E!DC--LFGprjVs$YD894qL`(m{2Ja^3K2 zd`^3DjLk38Ce_wSHUU~rZxFHICH^Q8kz$msL$cY5d+?iE7!Ip^FE6=aUK*wiU&Rrh z;D%cYPWb{nMC`Kgh5&An`Toblw$AGB&AT(7W)o5@)%7~MBR z@v>PU(Dv-2!8#f{p|O_+z|haXXFru-Y{Os;p2}bxf4vj}46A-d;%1v1(0(HNuNt5- z9%fGkgh9;rN0WGgw^4ixLb(9HRgW}TpcLQ`oa64NV$h$^06GWy}+uVxvK0;IIhl3h8sKz`&Yx!Dm-RM<7U zHNL?1VuNu@(Na4sLEMhIGMJ?H%V6GOH#p#}pVYep%Wu*aK)boj?O((G#z~ZU4APN2 zok`)kvbe3^o{XggGCi9$PLJ8Z`uf_#L26PXwx{U5X}}*WPK76mjf8;p0;{i5<8j8S zmW(eV?Z%IP&^-PCL*Hk;M_8(#DoLrNc8h}f16~&$vD((bTNonJEiH_x*6W|uSt*Y% zcG}H$9%tu#@BMi}WEJ~@=%Xd=r-rY0J(9q6ow(PgjjC5vwFM5DAOaArlwY24=t=lf zH;t@_AohGJw~yJ$=x5LM6{z2tkK7crSdMtkNJw<_@%h1>KlX+(J=u;dYq0o~RbZ9t zDGn!ME>fY}Sg_()kjhu5C;>ETuFk(!e5?LRP>wI*Y7&}!1C)*02@_&kSeZN=S@)J- zc0cz1D|$TxXZJ!3Vv9kJ!bX?<3NCiCPY3x_pevs;Bs5M-wMEUb1J%4`U;b%IJ41W! z)YguZcU;$_)9ejDkfrvIusYhl6h!=fXC=d#h|2R+oH;}^8(UbWWrnqF=Jne?SuxW@ z0~W4E;f2@b?#mO6^b#}Pwr?0hTn2SFA6mkd*a&arzw4K(%ufD$!K9LbCk!tmTxd4t zTsBV0#sb(SNg&Sdh-^(~h#rfFj}kn@?QQh?v^M#UYM`6w;VZiwXZL~~1uOTI5Be`k zIa8MowxzWkUyI#0pRFrPTrG!OAG2tKLb(f+aPC%=Ere(D_!cszp}63ea^40*Ryo`n z5d|=>?Q@NBjnyo-q(kodfV9yk)xI9-Yg4?f2g&$G;ZF!AG z9}JE@>}f7Fe*Uznzv>N7V!@tqzWjVgOn%<_hY*8PYX!twa=AO4;_qdet8_wo%d{E0 z5SwpH`|?yC-GW*r%o!I%gF+?grXTPWG!(0$wz}}#}?MdS@1i%Rz67k#YI@-ag&1N%v$DbfxmgzXGqw!uZ7pn76 zz7uXpPSh{F5w_fN$f&1#$cR9HDBp^(S8oI_yY!~AdyhenLP?zPgdzv8gGfBOBKm49 zqh8BN_H|PnemId)kf2h?cGg?-@E8gc<5+9Yb5NJEPwdQo!?e(B$~+= zw8}|ncr0Jp!~^R*&*e_{#2_oC=6U*yf#tc=kfajBg*+croRySPWGLUtL}j*cO~sY# zo*S;kvOtueZQK810f8v^cE)qr@&;hj2HhZSPh&PR?X&0zkL|ZuVLO_rij6`2i=4A; zEXOHlBG#0*Ga9s-%5|Ea+4dHIvb5e;eGBayCaP)vB*z1+=FgtV5a4jXfG<8H6&5#~ zq4jQ!rahH#@%JWCz;l=RMByTrf6ohzb{sT}ES1JGFz??)4}@Z+MWnciK{g(r>g=dlkyA0-~;1Cbo12 z9=r+!P1VrTM<-}`gd{9Lsb0yUtJ3K9Ih$pMg!<`_(dk>`8b>mbre1gNreA&n${>5w z+!v^(Ui0GWT>*aZD_f@~MVPRTTAW<%R(ylreO5jY$Io&RbJIpg{3I#qr89rsv_&|# zDbT@0>MrpYsq6cvoyC&+xTDr{Rm&;|8Q1{T%fVAW)3OI2{yaoR_WRf63&y(+i#f^$ zRvM>8QkK-#CTFi$WBXi>YTE6HJpG*-Wq;MFd^t_7ujX~{xratgAr{BU4>Y3K8UfqC z+!O*@f4K1sWznARra1v|Sog4Yx6nMc>8esYQp&S%jV)?7`@p?UhJZut*)%D0r##tG zTqjnU>fCJ-sSmDWmunze`F%Zyw+-h_y{Y-zQ$thY!FS40T`hN;ZJS_#j4x=%-*pu1 zMM+Z~4;^y+E_2bi`kH_5T>kazZblr=mM?=KFH>@1i#G`oo*dKmh@g-)VHKmG#JjV< z-VTdx*=O``rwtnRoq{{wE8-bseq5P7O+BrQyd3|m(P*#z&WBoR%ir@+L*^@p(M;zT zoC9e6lIV(VyVS#@mAo5ciMq=T`PAt4h{mNV=zoE1H5e8N`dy;TriEb-R*9({R#*Lm zpgLx@UG@ziPT%Sh1Q86sA>YEPDbGC_Zk&t=VnRP7bz|X7)rvc}^4(D`=FT2zLPw;d zd)W`b#!8Y^$^fQR3fho;4>P;B^eqF3_IX2|0Sr3mJKn{9K|D*u!I$x|Ji69sgOYP7 z9l^amWoS04pwfxgPPU=`y91OWKlpl4PP|=s6}ZanAB{DG*AQ(t|I^5%#df8@b$Re0 zZ(JY-0!gx8yG$zXxTPY5Gn#kS9E^g+iza;b@6?*0%;Lp^&)hVTgb~ZlG=xrz{U0E7 zZx7XSE(u4gMuZ#5=ahq%@GajZ-ylVF{&g|)4JuNc6P*bI*{tg`KrLKhH49$*t>n$V zO__BzAr*Qz3ms;2a-&@&4^riV!M4_DJJJaSsj>^w?Rwg5e6u%iDzCJe%Luc7dy`>R z`eNXh3Q~LT|2zPb7)1a4eHl8Vv61pTeqNy6t!a^Ch4S!WvGYKF-#m-iOT~%+`R_}! zXJ=k-|0g=quLK5ZfeAeuHLx6WYlcyYn>q7qUHma`<2To9i0-F~AimX%B(+v$tqg{%Ck?8n#my8M}^hKvjJ0vd|us)3IDBSS_&w>s<-&8Py zp}eu*j~xU1L_ZR~QrKA8J7yScJkYhO>PppS-?qH17k8B(@O+XwoooaU^t$W{5W@3Op|&f^g}WxY?NH>KqJ_A-SszpM)ttY$v%Kj#SJ&^1at*@ICt_e!s72 zz%wf7+dm#%B^L;JNmF|@glKw71UyLh|WPXau*GBk^+wR(W zV*b)`XvnxXDyUn+o5WWIddIP7Z(T*;P@srtZ53?UeC<;3cO^5U>Rn*FdBh1iF(nzw+Idbn zhiw=kM&Zdk*30IsyO4aGtT=luA0tY3P8=v|B1MYx zWnBq`T|^-6DdmWbmm3CVe8&5dM7Bv{;)fiA%|a@_i(7bdf8W~WRqKs?ORd>ukv$9d zym6mG5V+kOctYHTZ%gifFNzH(^Rr3U8h4>dUp7W{JDITN`j+zLyeX$8wei)E-Dm7l z^JaW=MHOj0C4H)pQH611;&A{IQ~)T_Cwm9P(Eb630Zp04zMke%_URH8wM=6sfX^RV|AoLmHXQ$PPf9K)WZ-`S=UfW7oE_f^)-u6X`Aeb}Znfda(@cb~$4+L{7V$4a?2@{k+ z_Aoq80-+E(pq!Wh0i@u0@mjZG%{<>;=>fmQJ?7WBXLHk&%~lWQYdyS>o<6{@E9{<~p%qU0^-AQ)@6(GV99^kpYHiL_MmoKCDQ0#P(8-`A*%|@4( zmw7P!;G+*|lOy~}Q3k3tXc$%!?}X=HeXY+w|6I>szR-?%Mc#;_Fyt%LeM_N;12kU| zF>`voUZIEyny-kMIqh{3S7n;7h=Y6~042C{3n=h*Au zEZt_wciC;!>&5GETi&I-muY$x%mBu_ZM(dYR@NIGZP*w`&9d2`k)GLC)|i`gUDoMs zdAt05sLL?A5Az*M*-dh_C|_rGswT!llH-7pWf@L zD*hZpOX~E9Y`hx7Y;Pa8q3vy|digk-c;a?l4tZ@;)zd+ILHRzonHQS_(o$Y)+YfmQo2OzPm=2Vm2SH*i*g5}%t%6NfVS_WIj!g|TI2bNbkR{qqroq9``9(QK zBfplIl#j$bX5MB!ZwJ%JQ%Ei64faWyNw$U0_ns}tqZyN}Z7AP5 zU>Q!hk&(}7qx;9?o$tomw|Qe!LXCh7na5+BA?sg3U;4>}UBpCTF+cE58xaRt#DTZ& z{a|0j?qFqp>bmrql}V0AD8lG*Bl-()HxWB=LWO_z~A|exwr$KEM^p_9^ zRf`A(=?Dz+o~?4sEyv&_w~5EnO|b5DXcV0AxTMTRhH4(VyiqyS$r`JuP~0 z-1Ojd(Bntvdi?Z}P9NOUaDJ|G?}0|X_Eipkown7HDl%~plqm;58S9M-2T30U+iERN zN)%sG791{>7&I2?f_3SVIH-aK`f7!2;LG5^L9p;`GQpZQ@a3@VE;WEPYwU(K^%NWb ze+&J03ZEVw>G7}zD?Q-wbq}22XN~WHx;E#ht`Ut`LZU1Mg@H%}qEJYRh&_s8k|Qhl z95~c&S+7se&h_~56A8K3IN>9qkX~-}#iyU^v(LZK3l5L|JAqLK`3hGQM1+DoO<{4u zTr|mLni@;qX=5qd^XXxFM8w4Kj5zQl^Nu*kA`W~i{rJW}Wv=*g+E@yE8tTfW3O?!l zNclAx<9yb>6}f3{TXx+^WuAP~=w(ENA`bAlGP-_>hx!I90&}&?NhRC%GQS%uGHe&^ z(O6Ru=Dp-oFh49e=H$0VTt-JP#Oa&(E_)q)-~6}vr@3!ay_38+(7&u;hsEsy_Lm#( zx!wm$zROH4H?H$NZJNL+DHAMT>`Y=aXZgmKj*Wq=QH0}v=XyE`7UE~-wZbWZ3)YOKTg);S&Yjd>rYoZf?&Y+Km87(M_dB^{vih57^#8zDi14T~YD({2Od z%gFk}n_8zHqv;IGCXBoVTcnkg zac)X~IGu~C)8N2YV#5xbKyEvcKR3a1x{;Ro#himhL_XxraDIdhih{f8P)0-Zes@GD z*rZ7zfk<8w$%s(Q=?L=^tc(Z*4h7EL5fTAJcwqA3%6fCCkm+hIag zW&SmfJ4!pO=aq5Ac(n}OUO)3r-wZr>j2c1P&t*aKZph7`Vtz^<``3|Y6v|B->MO1W zWPX7NEI`noL8Z$K@0%w?mLW|gn5LY0<73J35IW=J^s2M$`|tC)_C)i?}Rz6!Pkhe2eM{7CXa5CkaX2_FjKgD-?F4}dGl)L#eJgT%pV zM^G26dpp{;!c!%=4T_XBIPlR))Mcljuh!VCkPTi>IS~45;TridveS^Bo}B3E@v)wO zNB%pI>%te$rh#vf;R7KD#3&8#O%Wj;+$ABdEZbuGDk6{@ny5rf!E@@{2ag`=-u(xe z4qULmi2wi)07*naRN9{qB-&Too%F?5`s`0X%>&_u)6~`VmF>t;lG<0$+fqbKP^e!K zSNVugM0E8ld_=q=>&6k2E+V?<7262w8v1Vt^p)%~Ylz#V_HF(;EMH=wPajTqedjw} z%CF11ZlaBG)*<3~m0!$Z#z0%3^4?pfzX{&aE@7WjnmCBhF*ZJ86Dntxw(U#1=QY2MVh&M(u}--M;jb$*$?DevoKeVCbkQz#DV z=Lz0qj3EXM1LJM^pJ&{??ALYL>-0-MxAEPF_1AeH>I_?kDq5ySZFpzu&+k3(&AJtTS!c2)WZnB@RRQtV{poOBvMV2YMeg zg7gx30Ktc4=7m0iy=BhFL}P6 zz1^rwq+e`jo!hS4xcxeBnsHzk&NoBkH(t?EUbidcF@i&CAh)j$m**H7I$>vC>9Z-^ z?;W&ROL?VkjC|4pZa-6{u2JVE&j-!5N%&?8SzsX`*o@e~~oFJB{mlz8dHoyxb;}J9>b2lU%-^jz#koiU@cE z@}4(6D;&NPGOv6QuR@XS<21+^q1+9Kw5iu*29(m2ffNzeqK+9#nl|W~#~FjB(H@5K zvX=@6Mc;t!O^qe*^qsJma1P)yNXzNjXVwh4X@MHSTI4yEAU47zS0fuBuP2L#hJhmQ zme0W2Od3(3`2*VI0YSHeWp1xAKV1!ac)S5?JwR};RQZpM9=A)C?Q?1SLc!swU{_Zh z!D1zqwc_Ml^1m#4awc*35(lp^PL5^Xhr{5ISU4~m!NE`*42}F!=z|gdd^l->1o>2c8sC^1TWbqorjx22GT?*@WFpib?X1! zL_<-Ea=qL!j-J2Ni_gB$XaC_(_2pMzYRj6b7?e;^P>%f&f{6nEy)4rR2c^6s4txrD zL>y!h2Od@45eHesflnomh=VNRz|MUweuo>hnW zZLcUSgT=MHVRrs*8$@K@RJlM^inejZc4Q7iy_j5sHyW}meOK(rLw?KB7cWx#OW1A+ z+jY$H%rnb^K*Rj3Yxqh%=>|Hlxonr|O^}=i6zbIZPOZ1kAiwp=V;lG=gr54YqkN~! zoEGJ09?~w$2sRFu2W3s54a{Q`=xlEUvO+rcXxY9&83);vx5paf5oLz+;4kBFIuC(6 z@{gMIeZLD~DVoTj{j%&@MIj#p4AtFrZI(3YE}w5MF# zI!^+BC9$xX50NobC9O(2+q&F6vTRX~^71!`_zg;cMSFQ-#ydvHU zBEBts+n4eF%G3?VYzeM-j7I6^CET(SB5pl-kw2{JFA1!TPRo^C`%Cq0M1!cJ1A-?V3 zFmK$(TlcnD`g+^Ef1dUwA{22uUKdQn?Rg{Ob=lurzZ{%5@$QJb5E1WH|GRu!cT-GD znQaU=gsHLQ{jY;vhyMYXE~`u)qvyHKGZnpT@M7|0nWm0y4437#&v`qw%vi=~ekn8G z`1)szyPd|}s0vIS*Tip?JYQ*?u58q+`2_Y!cLY==FD9U2)6y?`{8ntlf+Tc)@tKMj zXy?On>R)Db+lD-S;cwh-W!P8UZHW`$cZA>VF3Cb!e2KH~##gS)hd)!1zsp=!^4OTS zF3bagAWrL(r+m}I?1O^T)3$Y95r^|++hh!*8ue@NjeHd<(!!tR;Y|fF?>bse11jiY zL+AoD@}&(vB6XE6uGm%CpYv99jYD8#Ym+cfS(Ut%jn+7BH436#>7=@*(5dDF32C$DM39l zO+QZc$0pkW+9Z$lj^;ARCu|$k5WX6HYF$XQTT_AQzKYk3^e5UAhvY@A1~tr+NS$p9 z9+HyJ8^t|OdZe5V=QSci5fdu$bx>N~>4OcYX(SzFz9XV2c{Z_Sm~@s!MCOk$ooW*S zA4Pgc6gJ5vVd?Y2h(*NZ7>yuSQ^WBV+9ZaOSt3H*qL7J(_H@S_sf@cxN1srb$HV3JLN6~~ z6C#%r$ z5L%Nl4^a3$4r$H%B3R=c+xhuD`5%WFj!!g}l`fm)KMooj4uL-SZB}adt9)T>gQOuO zzWQm_#nDDS6b6UM!q>!delBq^+!EV%yVDEGd474RuU}s1g7;l;SPaUE7!*LXSYu3# ziab>9sBbGF|It!bb}{N~qM+OuB(lqYPY`Ru7`0h7>KgK)@SKC8@$}v)hr^8sTfsO< z&~n0Cn9?5ZqvVSeloyP>A&OSRpsnNm>T7LigU3&vXhnT0eqa>X=yIoMp%-6#rLR8! zLYLIj^^M{qcF6rQ(vI#kDX`@kA(c=>K<>s+iiUDK^qR}%-UW}RyHF_mE>H#PAu!cv zN}KPpPAxN>zJ_dfanVqIGG3;b>+Dxa<^B>+>*>GdzN5eh`wcYH-K?K6Nv7vg-s?Or z-XC4o?VPL2UPpT#Q|Ie&+HihTqw7o?b(ql9GUj@PQd^rPy58fjqh|<%`beEf7>dZ$3b&8yS z!LT#Ldec#a4p{y+DEbR<1u?uU$of~}UUvGD@0DB1mVDQH;imstavEcT5i%>xb$i>G zH>~cI3{K7ynH&r(_s3q(M7fQhGalwWd2y6tG1)yB8#H;M6B`0~!*1Skr)SvKc1Co) zZ8~NFBy8^$QQ3ZI*t5dRS)cdGa|v<0jicZ+pX&C0swi54_a5DP{N#V43wn z*=z-6nsVx5-UnCHz-M6Z@@!|{PRnuAHdFnpEDK&7MsXhdnc;jf%QV}Bb@-KO0w3ra zeyy`#*He@cmP5tH%Lsm@Hl5%tO_k6$!!@kdLH*bjAC5%aWT=VW*I%0lrdy}Z;54viNabRBC$-|%WX zN>QT?MgPh&Nfa$#&GkrzM>`;}(;|JH-|NY8(;X2viTARI_tWnkWnfe4s!Iot+!f8D znj*|Jga+is+d9L_tF$qXElt>OGW{{lePuk(_onFi-=8+djA2imn!gFR>m4_>*w=MR zPXlJ=Z*qw2HlItmc?0ur<&JP4Ywl}ZxgsXr72#im#NEyM))5vr@Lh0NnW zr{irWFG#PmyGtLhsq$TU-$eHs_f)}?`TgN?dOS=1CfQAT+f^oY*O^Zlo8jbHY%-0F z=^p5*g!ZV6u-PX22EA>~+otn%mN#CGsdGMK&fCyiUcEv00)br6#>PJ>zXZ!R$ecFX zY-{R6U!XV5GAJkI*et32FVmAfzsD}@vK)N5j1K4VX1}LxV_74f^g;gAkuQ)LmYa%~ zsknFzT>tU)G)MhZqD#eo~HmBXjotobRy?t3{+HoaK zJG$L^p01-1xlV5Lt~*e^2s$6Iy(ou(G=X?62LZ$v7Ufvlp%Jl_?K3#z>rG?8kmf2#CA!cEa zX7in?$mcsh$nK5pZ4eu9!+KMr^MNiRh$W`t;Yld<@DV{W#Dm*Z5+wDSuEOR!0wKw1 ztW;ITb&eIrh`#_h-F290GH*#6^S{k=`<fx03=)UF;ILM-Ntx5) ztKFcD3SaJ|6%`vK4mY8JAP2XPu3v%R0BS)!3J`gkR@7(1f$(fC-Qz3SgX2N>k5@Y5 zP;|`KybbL&C=`^1sbbf3ygJhTd-rt@Z1}P#4v4W@s|+g%d!rSmsxd$ZRvu!j306@K zg24eWOvx+0qABS{K3vAI)|M0Cvlkco`o&AV`hZj8{h@F`Eb>=T+pjcI(nyI7;CH?~ z2EqZ*cNPu9z`1tN{RaLej zMrnnT9KNe!-$wIeM#MoAao}gQA`Y^M1OHxn2ju#K$)zJ|lx44t$6Y|Z&)4}Lm-8)U zQ#zvSt-B4AEh8-r+$YKKX!JRRd$u9|N$+%H3g%>-^;6pV+hLpA;dSILH2S%NLMi{M zFUvY?_bN}^)WLO~^rSIrtVxiCzY1@>>c0_#ZH*=0)7NE|f0Kx~yH6ty`iKMnO~&vS z#aD4-|GdFSVLUOAJTTq@j{9{DX`8<+a9w(=Oa3O=O?umn-f*(WG3``oOUn$)E0Z)A z2jos$-jQ2wMN({WB+Cu4SupK7tIem3{N@{ZZD+psZSy9FO}dDov4L;UiDz?; zJU8y9X~)80U+`^4W#d2b){!UW=Bp~U-Z(YNZSYHoctwzw+0MMM3*?TW&VEddmg!OE zYxm{2fUN6u<^vmydBKhnvTzz3({LS02W16RPNTDZN6L|zP8b!7G!s;ocWR$OIRn1T z^XgXmo&8%jZ3uX=%rddDXl`@zxt;A#@XqIS%eLK-G)CxzO8${eLo+2N(~jyi?KqNm z#CL^uSY|#c<ol<6Vz65ep;@!*4 zV=MaRfJ#%1%5?hgYZ7hA7|K@xR3LfsHh8C#GAT_BSj5aiY}}3{WHi!^Sj<;qu-#pV z$db||;wm3(Xd{Bnk5TqE5i#>6b()fjD9D`%h50!@wPeMxDo4(qm;Hd-^bwOiIp6ut z=F8@#tw^}-4iF&}5RwdWg~G9Gq7kNlKA><#Ol6rT!R>UB98Twoz`rrhMYEtT9&K4U0CX52kV77^fsWMgiw14r#$* zumIE0is09VA`gOVvaH|-`fq4bkS`eL_ScYUzAT!O)3~Xbh}6=88sOGZb;4)K^L6OK zQPF+q*}$O>*_zGt;Bv4GqYb*`>sBF_6ApsMXJ^X4C&=MWWsv5+hzRK>pl=+s3{Fi7 z=gY(3N;1v#S_L{1tG^JwxY)|mwoyK!JSLSWN(ib@p{yb$jik~LlGpG?-?nkfI}GjjXWl71s4Y=9^-;8=(8J&31xR0`=8KHCDDKxa5@Iw2G>0d?LHk<^cfctgFdr-;Zx;=nt7Hv)U#Ez6?yoJfAk zhu#%70jZ?HC!JG`M+YoAKf9kse@$si>dY?Y9ep$lc_rA-lxoP0DRp>S_HDsq;x^ws z0_&GNH6d=-%dwLpZr9W{jk-^mAm7FIl~8xDi4I)@NL)cwsQZxS9H8|oY&afBkTN9euc-T@#`s0Re4i3zzQMfYt|XaCf*x! zz{Iz{@?tX@bdQB?>YN5RMjvTU-OSUzF5B^m^hE2lQ9>8k608!$z$DZGE$y5Rm%)L| z{2pm<8vrk0e}P}IL9kD!O$m-N91{mUdBw)z2*^- z)@c=S;G21G){-J$P`mD6r>2J5n0OL0(xoD9<|nOQtJnKp8Q9W^<1r4hpq>#|We4rI z$3=u9UWH3+3aRT9(|>bXT!js_5rJ-jhK^C8mUCca9*qp&9T?$jqA@00j+csE1JF#v zLviumci$4p=16e+2xSyn9_dgNtD*wryu%ik>`qqBH-UNc@Pj zP2@UOWeRxe<#KJDXoOlG^K$M_KOSZf<;-cybsPBj_l_Czj<}2bLi z^4-9)B`V|To5a$$`R;pJ{!RR=vX<|_E_Zz=93+``b1y_LvnQ|fP8(hSZPAFJZx2|O z6A}Al#DU)ne@=`e`6f}^(08E^jN9m4dB1HPBaeF;fZujm-==?$rS5;p-y~b=`-{~y z!6MP~I8Dp2G&T9)XSw^(ywUZhmRs))uz4fXyfr%QGE*bvyi72e`hc=wWSwE&@C&K= zC6)R*>qpjviW=~ev;7i{7iYpRuPgvN=;VjR7M{%94&W-_sR&c0KQ zpO!NG6NmXdDBCgk@Q|`O-LR{(59{sQNS^YncRP0f)=fNRP?mU6i*l2~`=A$=TeXgpaH*d7JqejkI;94cQ?+W)A6wzCj&Q!)NlMIAqDs)C;oH25MaP z{1bJ4+gP`x-QM!s5E1#3>iUT2Y|0}@`)0%DO`FU|QA8|IuH;!Huqg>CBoO2^HOzN7 zZ8&XeOuXLfqKRp*xlOYhr#5rj7!k|34n;+TBEAVs^$+TO8zh%A*)93VJRvC*$rnk% zT@;x%AA&{39pY*e<}u%BfXbL1nM>Y~8|Eu>#qu_fFTzXMSEuUv0a|=Jm3i z8?=2={AGs_lc^6Hg_4Z{YD|3O;b5iEnhNG2k%PuMhLlfLrLOiTF*gsflTr&jqYf2x zSX2LjGLMJQslf@$|B~nthrtbppMkGtc_1Xt;LBiWL&+=Ad@79n&Cx~!he8cS2ow<$ zL;iHmoyV;cXAdo_B<;l2t_PF-eqZ z%~!;m>mv@f5jXR*TgvEMgYUMpETkxYNHPf?y6tMIa6hzL4ug$;oG{rsL!-}3jm|Hr z`EL^*FW3D$7Q=E=!+eL+hSR2u4u&41h`LyI6G;D9Lzw=H$@jCnK>4`e;yCWhL z5tFMMo)K|H9}#=2hy%YD{ydm>uRe`Rz>EB^JrKG>zD+P1BsN>`0>|Y6d>3rUHeYwm8LgRps#0@FT{l^#-Xz<_VlnCbK@JGPW&%f^W_eW^o=cfEoTqv|ofvN{KqRrwiR=arZN#=aeEQJ|eF2D)JRk6cMlngge6ggoueE=AQT< zkMt^klPuC!-{9qPakn23@beup>uyQU;`bo-e=}T9Lkb0THV(XH6T}q}k~fT*S%qy@#!(z`uLw|vmD;K zEFHcu(OeZ{Af`f8oZ05X;GoU0n!w>>4L?Lt=G86N+|PkkRLN_~O425z<02igMy$q5 zork_zX8-^Y07*naR4{Lkko%9NdQB{?sBqK@AF((XZZ;bopB!s*bflHP5OT=UP!t^M zB0>?Nl0ZgGB?i(1@(JRK5>hcUgZjEnXmt6;$VFT4siVvNjYE8~H`V(Tix7p~y@sXaM-`5BC@9V>R_w|u+cCL?4?}0OY zczmYEtF<1jIWTUHbWV9E)Xj&mqvDZH}o~yhlXWYu4-amj_5%HHOo{UvodC{P1Y*k2$&IXVIQm_ok@x zdU6}R?#G2JWj$?bSZBJ#Qnuu6%}O{1thWeLdWsno_;0ru)uduy%e)ZS&4plsT3B_D{#cO!FJ_M!a)u zgz~z)jBUNn=5>0C$vf+joU8F_!`!6z3n=~fm6>eeh?2^j{BjS?KIuaNr%fF{NwkyR zWf&HC>LSIgiZ>qYmwk#Y+hhud6;!ntyXvnXm z(Kr0c{^7OAW4Ylt8uAWtnRhXWX&)?_g@9^jJElK`&?#NB4p|S%3Cc7CKLJ}i1K~Ui z2w9@iyE8y7Qu}m<(o;i9l>WmU+xM9TYP0O+l>Tbka&185wLZWQJ`! z4aQ%jzGdLod4xpT(vR~vZ8Y*UB68Ir3N}>2Qu7sIPqv%|Z%HCD;>m#v0U?%hs3<35 zkmNFMhQg!HKCCAnwzlsG^b@ZeuL$RL@usiCjV=yCcM|PoZ#1M5J^czrJ(sO->T*Xd zeL4LKLebloHeT{VBd!SAGGMqP%*>eW!}&o{4mC1Jd)QUx(h_2fa?>Pj^4>(V1TY2S z!XW2xS_Fc(N02wj>GdnJ3{7R|a5c~4rOx$ndd@rGV8!@ebI90m7&ClOSuutN>KE8& zH^PfNny<#(qJo-!z~kM*x>jh64Q;o<#s{YrWpD7aqKtu4V*%lROr|HK8oVKN+^A!E$RNYdKqGXw z6>mqi?S<<0rBpA`ZPgmO3I*bjXnSKM&&W%((N-g0vqpYxb=^nW#;8ob5qQxUiNbti zqMBCxRAL=X(KXuJik7hLjg0Bj_i?vOQ=Mw~H~- z*4b9jN_-7Ww94(0JoRzg?a&!+>ybJbTiV6g5wFuEWmHy%25N0;215Tr-snH!M~s1> zFA99jSV_$7V60UIZN7y62<(8rf&E^oMFH=swkA<+HPVKlOSG=9-42RyB;Gn>r0;h0 z<4BwppvE7;zy$ddY8pzRT?HC5r6xojIvRDVpdss!gG`Voq`+B6*V)!>Fi;WqcTlb< zuaa*>wljz|tJbVB0$<*)g;<~xv@~&JpsQCzU>lebzw2-#24My8>4H?Gghx*#A2CRv zA@6WH%1VVV4hr(1*A-aSfuES*D2+PTvr*?pxg8WFDHQnZC}ofl5$c!oRobR{47qKY zOktQ$NH#ZYV>H^+Ehw&tz-M^{5kbDtZC6C3zAW0ZHEKjG0`9{gbHT1-MH>yJ$nxOn z&0?XgR>NBB729&xZ?#fz7#eJwSZjdVW`)ga*k~vq)+&568fKUmlFIlUJ=dV2>7p6L zP-4n4c`P?*Ys)8KSvAcm;)0WIpswESXqi9KFT9i!}DM8;gxzD%lQaj!uLEdA&@SW0L zN7&L@I$3YD+itb>M_nC(BQmwwwuxeVLtW=H$sk8|ZSr15D=?^xk~fq7hhxrT)OXKT`tg%<{owuy{IQ;LXnU|K9QumxCf;BN%uUD*RM9CP%-^2U5 z|Ky=`a-@;?l>Klljnbd|@t^2tpZ}S@Y!`a5Zo1^1(y07Eohp9p+fAzZ8p@z`41B>Y z+R$d!h2*L$XnI|{YGZ}#K$q==Z*>q)ggrD>&bEQ2q30 zIVIwcXGj!(1+IeID%+k+jrGi^yqhZhU^M!s`G*C-;FS?H!X6UsM#HiGcn#`kxhH^egui%0W!O5ED6j8d;R!RQ(@$Tow+~ZlVkwCc2pLfla`hHmHm_eo+U@O()2ghOa%yYkf0CzGLE57L5_=%|T_M-0`CF|7_kczhi=WLp36==C>>=*qLuk zWFzoL!3x^IZDM%vn+Ek32;w(yuolR9ylB~m*FNc%?H0tJY1_{b{?iseLhriTZcnYp zRxTeK+Zry@b?wxAwx?}pSXRkzYD|6McB~AcMsd(9dMq;C(3{6bQX?*I?7Q4*O12Po z(qlWRopGu!IuWz$V`{kmMwhvbTvxZ-bp&Iy;EC?!842-BpwmrAz4n)5bD|QfF@@+@ zCsg9I&8whogf;C!P4QZ1ns(S%<|Uq#i4?qJqi58saa zc}3VkvWrK9zYD0p#e3h6H-(mUap9q$eRwL^VHuk18^|RzGc_+?)&q}yu~{+=+No$@ z9Vs)&XWJP8_4o}p2fS~~^h$aXq|Qd~40{No#{n+0Y*?2cGhNQ*pgtubHglp5qH0^V zef&763ymD^nzn??NktW9RiV#=wh8ecc|*RMJZBp3j9^nUu(3K?uXW4;^k}tagEp`+ zEAoFh^R3(*V#|p~;v^KY#p70TIRU{(4g4A;MY##{vOxy|<{eM&h6Xut2~J#QoAqr@ zun8Pr?&L3oEHREibSLqkFXRO+%ObtF?IId_5b3Kac#jAxO>Dxcuk)AWoSs6E` zhWYNh)8_HuQW!txZP&!b_?7`1P}j=sS4a;e3drWLK;-ho0KtneVIFzzBcNGc;2B^o z$C$FziL238ko`>o$xoDQsW&Z-hjIoahWO#Pa_9@z9YO0QKPFu~*YWm+&N+}iU?sZ` z&f7NAXZ$#Fiu@Sc4g1dx^LIl#tSGNgfZJh;4Z*wIt!HxV*U@zc+GwjqD%or!IO_~p znRYmR8(jQ^KB7Xb8;B^VJ6KXYb5kUG819MhHlndR|CfP^>)VPG*ZcNa^1ZyJY{_?h zr#I=Y%a-ZeH^Nh2-Kb}BWqB+Dm6wQZIvsnONt1FWK-w|}ChFy4_KnQ>lr~ij%7Zyl zReca6-LRQ}4Byr_-mH_}W3Nis_OEc}^8m-#GGBeT<5%K|?wH3M_MFbc-&PfVyu8$S zxz&j6bSN|rn8XC4#tYCs?JtOlLGOVNXvsMyAs2BqQ^)L>97Cq7CONTUBVrwr(%`K#svAw~RDpcqur@C?jnq`$~Lm?_8@rD12()=qg!$&g zE$F&VZ!x{$ea9}ZpxCG|IT=o7jX!ED&3Aa zgwWXd;pA6eZuRozgo4WHz`# zd0&eStmw_iMjl1us*(P|4?oe#@mktl(56*lBP_nSe*M{}`m;a%Q+@HpmmCUT=#uxO z4}@c2Y99pNMA10IEx|<{4AnPCvK^69}lGvSGum^Bvu$r@u*R-}YDO-#yP@ zw=97$DKglMkyS&LdX6Pz?5J{8KI9>}s-a<96J^&Ve_P{(ulw$TNVIs@BYvwRSpU4y?ZZT=y@Y=>radA2U}z z=Fs@z_NAUMM;@7Ce~z7P*UYb&{WZpBbpft#(r@?SwqKUe)27S)h0{wc^SlXPl#SoA z@t2O8BvhVbtxNF$=85&3GzE_n@0W)>l%4v zTMi+nLSK0?c3tx5nt|L0waUTHRLCk{_k=tTapsW)>0pe6DZYJh6lswbwDPM>qugdc z8dOz^HtuJEulGWzxLyFQkRvw#@wi~-1WH;#9?{1MdqO_=Qq$b=pK%X zc4-OQHC#`$b-TR{v}LC-OF+D|xFdYuE^R^4LtiD?X|$ zbK0~mY|K~tXJIwp$melv+TrvPH_0utjp^Is5Le4Mdw$E8=)-+cj{H#MXPUf}Z8X1+O*d_+J6OpVTWYT27Xiz^~B6*19F{G1*UhZrKR z@)1|lg-yT`5wU2St9-AH`?xWO#GL7HI>Bb6c@6>74h5Fv>1k6W^4`e&)I%v*%z>-( zA?>dDy)LWhneAfVHu;`|rg%G-SiAbhD=GH=Qw`pTnFcYI;NZalFgO4zc6b0OLrx}| z2w9^_4Js&rMa8DE$$yOUm;>D@r=k-d25I9J8_a@bWBsRsN|4s#iUu|L$-|Emc$wcA~#IYNqI0-cd#A&PuB3>Eo` zz$TqgGVjVlMFTW8puyqIhrkDq?&;oxbFDT7&<1<~TqT;;90(JurWGggH3!3!%^?3V zwxdB>)3!(W@t49QPSk7QJ}CGv4wy}=uxX$o3-lj-{8R%m==s+gtY_2fL*ZvX`;$Bn ze)aWpUGUzd=hRk%>Ja2?Nh>?cI|3#mvOL(l zBO<9E5eMGso8UQVd8g(Lr!pND6HhAOL59H^N3A&US{#gL;lYtVHW^)TBEE4loJ>i z*kB)1$Q%F>8p-F*(Rp$b6D#ALQ7wO2Kf~WC^YR?s<3aikKgj&T=~_R1bf#Z_dSAc( z=v+U&cdQ?stn|a3v7=Jk z-xo{2=IKro;Kqf-5|#nCvgN3*++AjR8yX$5FS;uXio`*i#_o`jRwZY+lL0=jC3G5m(#;9!Sd!B=j zk)~nt@HVXE@qsT7kJy?Tc*S>9=Eugg*)KL^R)bam!;Q6dEDn>rD%%mgZ7l1MWxSB` zP-c4$cIJ&7SHxWuV*YACV0bYwMEeEh1cS892<#1~h3{scAZQN)Q($iy6_L=31tWQE zlxdc#l+h-nCtG;*j=7pHfsOqp?3;3IXY{&?m}EIHt@y9xb6YoL9o8jJKQw$*Y%DXi z+`Roxls2994O`2MY8hj)(l)lO=u*~WK=1S*e&xm{2F@A zWgBB@w)Ars?|e%vZI|-SFZG>Ymiwl>^LC%tsddJ6c~4t+6W+A!ce9%p?96Q543v3e zPF|4A8}w$%pb-{ehah6+ZA*H(C!MgJ(r^Ea2t~x(XZ&->5pjqs;=sG;h{(7jLU{<_ z01|QFyG+Z=Z3i0j*lljQL}mRP`7vvnhhxscHqRr|G`(d&lkXq){Sy@sNfBvANFzv% zlI}(t1SZ`n&0rwiAT8b9NT+niknV=j9bSd8f@^meWn0*nbFlIhmYk{E%~Av+h61^AUVM_5B)}-Z z6a9H0MuoX3hfA>)vtX80yT~(_ar$!v7u-yK&&5xP$?8V>hfD^iSH%`L2(g_nyI?$F z>`=~SWOYs)xiE|&IjX}RYVUTQM8~9fK!zc)UE3ui(#nA|bGrK@oHHfK`!*@d7f~wm zbj{uAXQZxmB=e;6*aJ~@k$qB5chV_huE8XW$zw~MwqQYFL>>;EP1l^f<;S;K!U4gE zD|k3&8W{nGkpV$Gg04zkv#$CloZ%I^Nhtk--jueKX$wN(N zo3lpi(w$aME1|9+8RahE!1E1PYZuI=I89CO*)YBl8%v`akmM$I9Np*VuyD%bM56RL z^d~^;r`N}kE#P!aoyW&!XL+#|akP{l7s^RevZLzh5?cMX%7juuHRry<0ENA^KA z1TNPFMy5(`DgY>ngqdatL`$&QuzJ-}^OcmI9}FElwj1A7ayvk~!rKd83I?yK-Qs7) zzfW@uaT|9r+q?Uo-?Co~TcfsLk5;i?=p-lkIiYA5bEf>+{uinC#CH3cmWbc^X2|IU z$3wcAV#i*cIUr|sI&0od*f5QR&X46K*evHafb<@5Q)H^3^Kfpt25g+XADSOpHxTL6 zznnw6kEqz1yi@?A4$fea&Bi{N1M*1m^arKhrbHd3!64MyFo3;lOV6Sv&J~c9qLV{h z8(w!To0ybpE)Jg?cBcmE53%tbk3J5(7z6)wL4$C)W%ZT#N2CUQd7jRCA-br&7|Zsu zh!FQuru~y!0^7EmK=+B(g*l+n**iS2ig|b8cSQT#r|A+O`niy*9=@wa)uXzL7iaA_ z=@BXmY3{mfoDVi!C#kN{b|Ftyxy8uy`E%ZYI+Ce98tY|isniZy#U8j*R#UKPTUMw_ zF7_5(6%39N1~`l}C9R{18>>}&=f_LzR8rhyUYj~B&r2_Bnz?0HHTbq^-_op7{Pp=O zQe}ELBUmhS0#9`3lWwl|_TI~9rnNXA>tH`SOfPMw{kgvx6L#$t2M0HYl{-uIiNuAK zgD>{*^`v(8c+s#Vsu6?lcyMabjiWBcoHsk~#GM9<%pSK-;8hd9q5dR{M)k+zm31H4C_)ysihkI9)*5$|Kc8t>~Yy1`)H%&)i z2mjR)L}2YQ9H8zF^I$`4vtMsB0+2G4ua)uTHfSC1(Mv-961f`H3D^_osSMoK9sN{3 zp$7@NC-*Bvi2v<;wHBrtQpAc{!bjmTo$&nUML4Vz@`R^ybG|A3@R_M|CFu{(d9cYJ zPWyA^3|rpsFpj_wJfC!$zCkIR_FfZ@H>)$+`S93#3Ox)kCf*#`*L7g;*&{~cJR6>~ z%8CMqk=$f9Y!rC`nJI=Ga|C4{&43)3JlO(eOsdOi9g9fQ7F}G7-bifI(J(g^YY+q& z5N_~-l4Z_cITZizo-i~VuX@#Ox*FCUBN8pQpx>FO8>Y2rb`RUMY?Xb;Br?YTE z1Vsd(M!h*#S%Y1ay>58xpQR}_nWC~P3OAqTZlLP%@HA5hx=M;*_th_wZqGvIm)Uf4 z?xA_&R@VwxIFS@zhdGZ?2uV!T$o@UZwf-_ElQBnXma^Bf<3dpE_*6YcrvXVT;^DNu zfF{CTdD(%SKfk+*@oZEwi(S`8EyPB%*g;1@Znyafbk8#hjE_lY;F5L3Fy2kN=tHO9 zv+O#w8u54VF-cWKEao&{_VFJc;D`~-3GaVmeB3#f17uZPfga39Y>1=@F7ywCIp#;% z1$iC$?NiUf!X{2j?N9%%Iax$l^}($8hGs&f_22RcHXAShno)LCE-z{?Izvl21Q$7? z&-<`IZ-uK5pTx2IlI=G)SA7BCwF<|u+6#a5X0^`$HmjkqR%T_r#l8EXI-eH8%63fs zqfDTkrC^UYnp1+DQeLvix%EDGYegYo9|LW{0zrY7|AjMH`9Fd#udkg``xY1?L);gM zFC58>$=gBt;jhoyp##?C@i>XduW4C8UKWnwJ;_vEETv(Njq#w-(Z}(_hCmO2Bu|z| zNqms24VBl&FlUZpzn%KZnpz}l4R-UG{HgAcDiuKj%<@QAe?ZTA^-Y3r(iL#2$~-IL z@C#55V78M6O>V)BDgNqiGFLeE<>ml^=XwciSYDgGb;)*%(}|(KTJ}Sel)?})>FYFR ztb)CtMx3^?s~V1c|9@J5Zqaxba|W`t(%j{HQ;t^!+H(6}4ONcZYO{Xpkz5Ci-b2aN z^i>*%??;5wDD*1jsB0mm2^MY7b9Iq#qm7UK8C8_(CgHv^Z>>$fcKG~Aybql z(%H8~iX9K>1k=9Ck74D9@{d;#84A?=IU*ZGE3wuFkCduls$gYOIykt3{lUP}tNpj}>gXCH3d!5ocw%^|LA9Q|A%HtnH zUzGLb10x=~-GKY?PGfl4s@k8|Yj#A~A*(rclm(--BE(^Mx?9)<@zrQa-a7ND6~s}j zKDeF9IH^_VKCM))0|Dnjl3{!aJq8zo-@bum(nwHOwZ{4fN=f1`>ZtRa(3Aw~!V`Acqo5OzeX3yMJH6sAwN zTtvk#8yA>LUWO)dc%-{cn|97V#70&D1IdCbVs*%3B!VgTA_&)Ol8YowrfaY&trPzJ z$LpjUX1M$b<8S;+<59^i3zCJ55cI7a0is44m0x93+0VzSH)n_^(O+2|iAJXl&u~_I zWO4RkECPhoM~F*wpZWc%oG&aRc!;^+ zKS~wwQ#hjP-4`*PL)x4M{>ur4*IoGgbrJ{0SZ4>IaS_MJQ0Rz#m0NYsrB;@pf*1vY zNv^v>T9Gsny;)473$epib(P5e*-wPVI*G7Zb{U+pN}>xZpWGjNv#9Du!{-walJ_}t z{n7Sun!^vY{@f$$Ju=@Nsycx>r^B;E1pj$-GvcY<_~HhN@{3M$tO!9pRAO$NNbVJw zDnIh>DT{RlKSd}u-KaYWQ#yyy(z>uOiqI;pVFla7=NgKK@d%r>_PH&~d(gw&5`>_* z^CG7_E^Hn@SNdA*K3<{6pPZq7-M0}!zEE7p5=Xxml?82+~{d(98)BAtP)7t}N0$2~a~ z>UM68;pT<@VmqKws;F;vZjSU2BU0s)?R}ocEYm3dY?yzk zA1a)2(pzKyzqwS%`;onMHVwkZua>`m+Ri)_?vvoa+&e+J))UE_XW1~(nE4yD1$>y< zV*yP2f|8KZ3eqbSrhVm?soHkZeV?=r?9Bwn2M1QlD)f@7jf?EFQk1wB6{W(+gIvH+ z#^{Tb?T)(y@Frnq%U$3$8ox8AXA+-I#?4N&mMF~7g*rp+O6*{Kz6ytIS+KS|uyNy8 z!q)3iJWD3+oo-_4)3UM+>i6xA5<)n3b1{o0nKkmJlLHLp&NZc)EiN1H9-XAVw`O8J zh+gpgRtVDOcB20pqB}25o{o3_Ld42ZgHAs`ia`R6#xl**(vh}3Xm>f15`^i|x)Qw< zqx?rc`m*L@=P&0W1e>AggOPc!oxfU|oQJqZJ4lNU_{aFx%nW=rmdts~hdjwXA`$73@==Z~X7)1nPR}Y1 z#E!-@9&#`C`V4u}1)T>8&zmRNkT&$r%dLg1YfHFqvZQeb_08r`YvS0_V|jgLtls^H z8=k5MrO%kJ8Jj(5FP%15^wTp6;?;OTfZ_M69YN|mgv$1ujkGjG=oX!hxV1U_-u8fj zEPwc%w(+9IbmG-yQj~cvAYf1&QBAfG`2gDbvX7DawDDC_*z7Y|EJ;ah#C1u~>Gok_ zZKK6I0gfQ{zOON4AIIn>>7ND@TS8)fE>UUWtSdyNZYUA{eeYGWIbL%4#j&rE6bs6t z?&kt70DzG`k2jzvt>+_0`0p$*Qb=%LadSH1iHs`4=5-8J#sS5bpLfW;-Qf!b07d1E zYyF(?-hh03?T|yo-RcvOoJL1|8$v@qC=_`j%6+jPD}xK zUJ3i=F_HsZrepRNUGnm#34$8mMMIu-KD=n^8S0saA6%9Oy3w^J*V<{tW0=3%-IsYm zHam(T2EeT(wB8i083oeDTGRV+0F5bu=0(PU^S?$@=nPq}vGwL6FDaq#lKC!GZW~zL zcpcN83q*Xi()cRs@igZ9XFrP?D-tT-MmJ|#7AVX3nX9N_KnjN%^N!=2SvVQ!4NHlm zCs%nYIX-z&t?Ws&X+?8>U#6F7;(MrUd&|GHzUpvTJZd-lKySF@zV-!+Z@0sxKQM0IBvP?ydBM zn*~~zQLO+PWuVKhw zad$vOAYi^ zfxSkTMHG(>lv*Go8!$qmZPbwJcUHn&je3PP%S)y}IMoLY7Fta?yrEEIwVy4*@3#sj z5T^O$xZa2q5mnV#w;kQkJhETffmYbo^}n>GC91??RHruyBtuT7xL!&24Bl6Q5@&Qr*oGOrakUPg~$o)DJa07bIMzB8fA#Wi0$VVg`GTntl!i5Y) zmR$@ed4546*K3L10C*~8)}9LA6Tw68IbLzZ8&D@w{)IMaxcKmMnledCzF(XdTdu%f za^lVBS`$h(^_<9h$l#-8@{pK+tt5p%ucd2b@qmivbLEa!@aZnxdS#y0wC59mWK&K~ zt{+)xjlzBOCSmZQdxNdkHF|b)0t_U9A$t=Y?Jr7Qz8Kx!nA9IP zs*s|8#gL*iNA1U+3mE{e79bn@acxl_@VNoNjiiDmQLYQDosYuM8%xS7r&;wk>hLm7XS+N@#`1rmjJe`)c&RU*fA?Ye71 z7Wp9oa9`o)O-7r$$pXgMSrJb*-)qgsHQ5fI_hhXW&PX%yAb5XpETyWddOdwu zD#9=D&-y*(*H}{wDS=T$^8}1{-VNmj4;ymV8)+L2%c)#3!jMwBC2*8EVKu391HE5T zn~#_AbFGo!&vE*VPK}keKFS8cSO~38^ILWr3mH9}1&5l4KvCc=jq5K(16=D|_I`Le zR6I=_-`QY-U5OoKrkXC1BF2f}f#ql^#pY?fQY-~VF0LMXj9A2J4! z(kUlYC@g$%RnfsEf^M1?$EQ?#0f0wrlkj`jEb8nl5@trqd(OmJny)dtRs0XFhI>6S zbEI>UcaDm736=qM(I?88qIP_vqZyNRc_R#Ym*uV+aqDk02-xoGD6<)~iu`Vb&uJ<28ZMR;B6PZwFM?-uSJds~~UyH``Ev5Q#x^7zRQ z!dq{h%#v|Ct)lP z8&8TBXNWFl$wt&jqQ<;tGS8(P=Dz2G8=aUuO59lq%55{Z$+^F=FRkSRx!wG1*D>@& z5K82xo;-4drS6zb6&l23H$NpaF5Y<$6IUx9yvC?jMWIWJDO5x(>7IBWjA{bgbn#FM zXqjENJN-_tIpP@*jRk+$;OiTbtu5pIfv-5XQ5FCXZkVpI)g1FbKI?uNoeI;WncM!y z;`{21+^vP41snnQ@aAf-Cb(7*=VLS(5)kZePh`rAEG zpw6KvuH>&Sx_ma>R-=6q>6X43t1c!1#F8M~GJfH6MMg6kYsna-~`PQPVO}E7l z+9=nS%B52)BHZP7z&;IDL=Uwy35C^Uz?~lc4}7Eet8k9yY~RNu#CR8?>CqACh{Stt zLe3-G5N*i)t~mfQRuo#le{F}olq62qfQqU+gcKUjq(((2K zL2Jo3uH6PN98l2~t6GkoqL33M!Waz7)2bDMTR&-6dIHWvItFj)haW0ZD^wbNC#@cD zd=Bl0`QB0TAfu}|hXs6$3N-eXF~9_HPK=6de~=$)h2iZ|1Q1y`=tXUT=qB`JS$#t} zk(h(U@P&=)AgZQQRdiN!HbJi7=sZ@9M~NiBP+zQ2k9VM0wAY$0G{q^=O9Ml zK6CSFt?EeRs{h{eM_K(>!Eg50nXw{scaUpS-}7F8&HaJoA6Yytfbbkk^#z6$ z^hK-Jwc5+j<$q-dt{pSXSMI$lAoA!qW>#uQ+F6)d--|-^7MPXhY+cC2Ioeo63BBER z!--$kx#F?%W=z}Yt=nVUrTeNIz=0el$h&TwHVsu=+RbElQ#5J`u(Fvd*YivIe_FsB zs#4ljbCqmyI3nfFAq( zwW5f|KBv81gwksq*ZECwvKv`@Sf)kyNW&-;PbA~i>0yhw6y0onmeInjb##b$uczit zv~G%bgye-=1CCcDIZoS{s{RlqB+xx9F!w=)d{+lndxt({pEWeDQkbv;pkrrTSYOL> zw|#2Y5`UOr-Qvmod{cT35I8^DNoX*S_Qb)GYq7^pS83>(5ZM6p!8P9Bs*uj9x~J8x zVavoYFFwS3St0~ms=gIDZnwp@-3S8*<)6bxd-$4q#)RzI%n3P`Zg9j1{A3MfTV)px zRK=rIx__kT7k2XBS$oqH#kF1q$YVTY#5`gHgE?(M)ydYJ0Z@i}_fLvpn4fU2;R_jQlc#+QC# z|C9m2YnOknFCaa{-6%U0@HY$a)0Zk1w^i%l6^tG_vIxMPwdk}85v6SvthoZjFDPo& zZ{zpVv{2sFyUw1?ZoZ;IH?%T!9KfSpxtTJTuMWMY5RAVSb~PdB&9g;vH!Dw=Htxcjm^~IaIMP2 zh?6V0pibZV&!Ux^OP+2uJ?_LCU?zBgx+7kDXtD3;UQ;M$WRk-@nkcviuSo z8_Oo!h7Bj?DTW}DLt+_=L=QNr7(m*395kW%KF#+eBeM?<*$3tbN1-!zcYeCwK zz_BYzXE|9VFsd?ihL+Z0N8{lK3QfyOFWVZ( z+`fE@{A<7h0+$t&-HhT*Ugy^*`E7o@?!6LU7btr?b%ruBO0SFR9r{FF;f3U3M14X| zPvcBH=f~`a!Mgv;uaH^oUno27Ed$P8$!$d%j8;Qrqh@fPDgs_^b+rh3sQ=Hlo$zafG`4_IRcqtrL2s;P zwjYx(CO=#98Mxr`B9vv{k}M|14Lk-9uKL|jCPw3|a(PeR-o@n+uhW{o@y{YesC@c> zDD`X>4#LE@^m(Z%cpgD{V>EzNo`o@>=thTiN=;qRInqqN6Wx(<58dpC(v*^}1AwVu z1-Ff*9;)XU`zZak{lJ{E?!T>bg2sT~lkoG>dWv+JtT(r#;^-Yyyd}@7BuT(z(gOmf z6E0D&aGGZg6l)hBat>388?+wW2+ZDeB~5pLp4l^%Urm_u8#2v1$f>a z%Vy8gJM+wX*6XgJG+u&g-m4f#(+jkpc`EmkN=5P#xE_!8zo$c0e#_oqTdsM>N%zTD zXO|6fgpgG`t6(v?myp`2<7mcb-v1lzJoXwm>*E%M-boeL397QVX|(HH5}y5oH>Mp) zvbQc-&rfU1-TJY0Q8u3Lrt)O~$GY%P$*fur0_UXI=HI6w1o02F4n@JrR#HQ*;bDco zUzuNv4v4dZ8gM?Dv2Q-uM%kGt-MI};fW@HaBfa*JbBVXy^f0R9@{Rak^CD}n*{k-; z?=Os<%qWzsxGF2?jrLy()@WA?e5`EB*Un&z(S^<}*;Hm~+m{o85a?zvO{bAoW8E>O zHETrR4`wP?fo(wPr3#61@k>DIwN8)N*^)Y4Xg}6+onH*=>n{|nSg*kwS8%KR?RPA8 z_~aPkk|`9AsEump&W7chSPJNkGKat`BO^ItH3;R7ZO*%7XUz7`>{=X+OEivtI?hfk zfOaIU}k4$!;Y^t^&LOL-ve&vWLf$E6u80BK-S?< ziR(*oXOZ_tmk6KRE%iwX)xzMg+^uzALY@IJadI-3-dv}!yz_1+3iCpaPQR+^hj;gI zld&#VPx*6>DTqAMR|RRfW&2iUz~_h2_3P*FYIFYC?`9)vUl0`1y`h#CvkYU!p24S| ztRFi}Ow0a_qpT$M;RO;mrzirmB`VHrr60xetC?)eaXg4{V%Et-<#ZX?cnRW%!@@1L zC;^87B5$S)Q%?@+V6(R`P4k4G)EOy8M7*5xK#v;%oj5!+(?4CKwL%kaRANKR zP?ST{fji%UJ5JoUJ|bsbI}iQ*78|toSaSgejBz;d^9D3>yV%998&NE8Z3#&K@ z_ohp*-|e4}{YkeGJZ;rfQ9yPkZ1^I)b28wp8aa1%;E1T~%8%_rN**3a3AR;l3S~2$ z3M({R$(G$cnA~r~@AT4waBgrG8EQ{wrcQAjTFfh)KT4af34xu%dbXiV^zV80&}; z$iuu0Wht(-z2~%ph$(i!nxy9WMtE7w-J8d;pLneV##RR!h)&YD4rYp$@I=^bPbE$G z=;?O(>f$`+BE1`5)oL&fR`vfoUrkU%Q0J?pb)Ws6%L@TP=^YYQKYDZ)f``9ZvY9KQ z@+YXo{v4^|;;*LD^zNDcU!cdv72T2KcDDg_3xpdN?C$&6jHGMRlfaK&y4+X>@_|>+ z^ZtLZ6ZIThPH<-hmwEQh$A;SedzB`G{-k}^w&#-|LEFBil>9%TPVWD3Vu?{f5fPKT z!uN)2bV)FIou`~juEK+0`eiHb@Iir2u7_>+uO{m z{?o)i7h!=*A%M}jip-ZMpViI+Y8#Aj^nY*_lv+#13e5?|8XomKDB?uCA7R4QOZc)G z{sLy9k((J4`q#L@=DUXA^VjM9k-+ujYQH{OT*wP7&ZLta@Yn|AqEDOgsmtXoQ>FidV>5>vDg z+!!un#$uijD__;rj5?~F>1B)Ru=$ycneQjBf$x`!wt?wy}~i2r3C-~ zwTjg7P5r@h`0_C>hxXO77;w<7KCjFeTpA0YJS^@ykN3MCM$HD_3kNNg0V@e2e*6cB zJ@w)X3E7(#71z=qey;>UID&sm_jNyq3Mb!&m33F|-*zu~iBz~|x42}tt~<`WJjnm2 zpc)%Op2Lw@+|t>P6WCg9M?}t?4a|Yw@79r zqQ#kkBsnatlKYqAQ%>s+@$|Oy)fD-?Ax((c4I`G$4_L#~8Ffs2vdDoqu`P4#-6~{B zK2&}4*}lX>Nve(QiY*T@jbZtN<~9cNdUM8^Uhxn#=kq${tj>g|AjzcCjDafp$}u5} zB-lQWJ6d%69N#5sbo{Ikk$EFQwNScz(|#a%pS~%-MN;${Akb7ZZQz$IzGfquyv;2p zzSo_iXA?G?uy-opHMC)Q!#YR{}zRjU}N2Cr$nGVIg>Z)A(UG+PP_K3V-h z$mshhak_T6h1A$L48Iqp#}74sg`GAcGt#j$z11YZA(qwz9X|i;*gfILv>r}6N2=m( zgR=g-L8SndP%%Iiaurnh49?Yn+nRN+;I?3w(l;b~4PO3!$Hbj$v7!*_WB=QMuhuGg zbV40Wz2mg%pG2&Gi0UsKD4K2w7Zr(Qo9!_wJr2YPEO;7_Hu5I7Lp^#Bf_9dj8dps; zs3V-udoAqXnANM5c1w#KMoWA39ym^b(OE1MjhW8)f56jhFtat#D_Oth(yQO|2X8Gt z_-Zg$mwK`PQoW8G-f{jdYoENhCfLi>oBqpusm>$d*-RvZ0B7>hogz?TKw%~XXT@UP z)m7lgb@^J2)z<*{NDnmY8cV6nt}%WjAn!P9s#)R^{O z=RVAU_mvt&zSP$=`1j>#wGoovX}w09Y$)DEzr_vucGX7^?AaoHtRa^;N#gWn;Nx5A zg(26!!kwC5z~^G*00fyfr<|S3;$1{S#*TZ=2~W^3zQQQ1PVlV(=J#t!kxst zOu+t}MDKf^BKle0YMl*u zP6?m?;)%=#KG{yFC|fdW?H6mso)TpOq6L?u1odJC^|kCtbjz*a3u?s=k$7h*9a&;Xnq=AScXIxp768WN-y4i46hX`2eszY&D*XNA;oy+)`5?vbYuDXV zSGg#BA5sEAfZ&SH^~haF8FB}58+&ty*cF{Qd){3Cr|NgsblnpR`TDp~>gulQeXa|W z1#H2yO!$l1=|VWJJ~GU1$|Y6baZT`lz6_jryX7;1CzsO49Qlc$9h0oXC|&rt%f>a1VOpt)YeuLe;!6*V~FhDnm?TH@%M#{ z%rceshSXI8YL)?*r6W^)CZ;@aS4|*pw5zfqO{_75RERUUnF4?yMN<%Pk7?3dF9=!l zk2fW@-mmvXBZjVidH+3~+ZfnHe2W9}rSM$)?w{#sr+n+w8b5#Vz-W_@+}S#D`Nk|~ z_2S6IA3A<>t~>FC_amMChEtx&M9Mj#iVsx%1o?`;s(Aic`5rW@Qcadm6hs6}WW&M3;n^e9r z@;wkniuuAs!?t(pXf+h|O*^Ual}i2izeC{z3O~=jr6oe^z=kqB$299TIc3|QHo)dK zdpCxROPW${##F+V+a%=8pA>1>?+YTmLU|mYuK%jv{aCb>DqDLHf3Noo59Gd8_cop+ zkXbaN^37Ek;gI|%Z#CfK!M%Z~iR8^U07MmnY|B0neYkIX#8`i*Lgb?)vNI9CJdl7e z^iJV`0Cp>oUSfLG6#yWyeNe$H#g^_5rN*jkCucpo41(>2p}ls(p-g9f$2(U?9pCLR z+T*xGtt+q+4SuuI0cJ<(ws=8whu#-xi$PyVOtK*csJ@PeM{N#v6wi+6ST2@>qd~HX zclUE69fFcGW(uot{yjb2yYT{Q?N3!4kPKow@9u0p6r|Y4Vadj7qY#2w`$RNA z?ccZfjfW4@_u@te1!sI~SV!YfID|1I&lnEGRdLEKQ&W83=Wd-&d15GFpuN;mrDHQl zei$^)etl^*<<4yt8mkau5vTJhXf$ZKxuk{U6RLyiuf2l4oNXs9ZOdWWF@nH3PKM&X zqvw;7sfufo7b@sThuevwN7pwWIn`6TK^6{#?f^%{*>Ew)b8%N$ z*TuK4O40jJNRB8x1u2DugPtdEIFNkEU_?*Xqv}mM^7`!12eMy%xas(;yWe#|mkHk? z?7z#Bvy{DzW%iodR{Xw9R0NF&R#*=x*bY4Ei|d!uS^r?8%(hywyN(h-(!&PmYh&JV ztIRsSzSDu-K023t?RM{eJWXI{CWP9ENADc+FL#h1fGzF5ti9H|O*gl5EvVt+9pjib zFB(wy;8Q(bi3W0hw|JY1cr{&X4| z)9srGtb*imZWd%^D+wkpZ;-b#j6R!SriiQeq1uk}y_}V;6}r(i`m#1kC^cY51jBJJu{2(nD{+@-`SUd zSXS!TQ69C;!AkCzhA(O7UYYC{N^p^xQ>xJm{TF@D@^Bez{FRgAI_krsmtx1K&0E2S zy*oh|o*YQffK)^E*Q6g#3N+56dq%3D8Op|t$NHgp%aP{nSUuaXBF8VYD_p%svK1@B zT?obNT#}XeJyUdILuDN=a~ILE(N;CC0#pHFZvbmLbVygPwD<6}3)l6~f z$E=^SsefaIdcKpZo(Q$FVI)E$?CKu}rYHY<$iBktWSLuf;d^~TR?pOocHcZyUQE7R?R zYTq95h8^Orw-0ATK-{n-9K-F6g@x}}-f4>gfR7z?!PtnGhiASq}lvBv>|bIaY0)!K#^}oRMd~9q~+B~Z_fj&lc&M}FoS{l zzU!ZYh6-g1jYCc09W~Y;8Uhraq96R|gzQEaTd*G0F9EqgvJuwziIWMM5QVDZ(dc7PMXtaabB~~Dn#!`?{)|I!Vr;81>ye2r ziXfo#-#S6&k0S>urA7k6j^#%O`?hbs#f@L9Uqw5L67+40{I}+_ORT>adIx;7lr$|N z`c}$B&6tAZ1r8CI1oeW|x+Lb2={z{;6mL0;!>zaAu8OK^krZ?b z``KW)7#@|~XXC1VZ8*AwUH(tuiD8S%t9sK_A<^l0q=Dses`sSkvLX?K-)ts2e97_o;q*)1&uc;&UT|Gl@zB}oy!3lRU};OD0;LXl|B+ldKRRQ> z=9}N9NiJ+jbj&e3FSitrkn~qbJ!bD7JT?vqG7Q=BuGy6Ne=^zbiDWeSX)|h3qNH?q zk`WJG609<`R$RPSv?nx%|E_VV@hLVyXV{!!F3vU8K@)LPMm$hXr&$hOkB)KCi!~4sTkca%6Zfm2--R&$xtS9v4a)w0f&Ib?MIE;>U~9 zf7!zN39K;k-uFHHv-}{a~r=r&0kDjx4-LVfe2uJuqE@F;D80GD*x_08W z>CbgC6OO{P2g=UI&FDfG@N9%X0FzlOVG89ytCHZxw*(R8GT-{Km35ew!;i|h&RB6h zCf~SztX@*sZalnP40Z=~srfvp_ZSaBGmv`Tu+;44ELr(ftS}C+VI0`R<8+=P| ze<~l59T@iXo(uxxFN zD>5i34>tp!$QI}w_2$VjY%0+o&Tc9v&#YyA3wc93l4Rhq<~d}nC;CI7J)#fwle>=( zv9*S?Q;KUZ%Vr3iD+WfI%LMjf=4~_2iJSsslY*4=HRiB9;p(sR>{)h5QzLs9c)CNM zj1h!_;+-;-WGAhGeDFa{k6DZ#oI(9+gz^=3_B?Flhvfdb%e1qoE}^Q;JIg^eiCuJ8 zfsRVtCwWNyIz>6I4f^<}eEf0cnu#g9^{_$HXWy^kgH}HZ@}31-#~McS@H08zKkAby z64AwLW8qvQTPs}Ovr@`VzadpUhiDgQ!1Wuy4tuGm3d!nQLw|NAV%@l;zse#DG&A#y zWbK38GcOPgs0HA8(&+SDd=C2={O$FyofND)Z1v(Ijs0P>f7@nU$lUWHNko^=J_}OT z^_1RK0)T*3Vc&5N3sqw0^*2AJio!NE$r0qK09mlP>B1G==={>Z>VI2l?Chew?uv$u z`h>7Iw)UGHW);>1DSv!!lRA58&QQ}M;6U_g9Z`L)iWo)y@p}?|BKE#-@>9Klg5o=` zd=EK8JL)lJrdwPe)dYP$W{PN2h|V~{b!|&r+5Y{BiJfdPaxB>@3AlVe#y7dyI{kS? zhg`KrC<%D)vsN&gH|ckqga}2hXCK9ObxvhZi)L48KSp@n{DMb|prURo*uIiFeLY=< z-eZrMWX^H2L0V&zjiL6~%N_;s03hGWJ~uTs;AlLxcgI4+ZNW;$bi*x6iKl9f@)rg4 zDR*<*_dJGk3T4V|GMKgqJ{gldWJWhgUw#`QKdRMeK{@NST=zYgx$Zw&`yg{}4#7Om zhm#r_z8MTZ=m+1T!+Pr2uW_#t?-tB6<`O_M`^v*~b|>ZZ>vGz>X5)=JgHgqUxUDi$ z^#}i~P9=my_ z`dt}MP42U3vW5vUCUZ8TC5!HDsGtq~rbYqdTC2>L_dmhEq8Flu3?MMuOOtr2oDXRK zPYZZ!cMPd3Ur=^hvd5nt>_oXo5^jHp4BBOKt$u7u^O~Y)Vy+Eu$MGgwAhbBjQTb%- z6+^!7`?^%xTd03`^E4(tweUq>o76Df)#`)Hn2PF;!I`_j<&(?}ek<;MBF!`hTuL66 zHLEO!CNEt1rRC=qj3ha)xp!?%g^Bx(x@ajuo6bBmP(ObdJ@v>tWyhb(9ulLE%~?(2 zddROu@uqeA1YSaDes9Kg0@u(rt6Ktbg5J+WQQKc%|8j4!hGt;+j5pK5ysbQ&Ol^cp zq8N@?lQ4@dcxDt{RX$@q&7d`M zYZ(cLPW7Svu15Fu;SW`$=PuZ6>ft(tV@t_JMEra?|K{X2GxYA}Edb@^5N)rH72%&w zD3%q3fD7u28NJW1b@2d|HwS(P!yQwDV9xuUi!>?<96^h1p0LPAjL5(KlQ_Cs*8Kri z=M4Wg;t0`qvD&C3Dcn8 zUv}OJ{HtE1&f%jqvCizwaH;b=64<%OV%^TBAk_u9CR{u9AD^p?RJ*WA>R~a8qdw*nBriUz)dWsQ1^z3;m z1T>vVqVj!OcQzhhn?6KVckPMYOPD`Nl&65KJul1=hc5td^jj-B(Wppas8}XtWXYK- z6LH}211^2w7kyJE zlD^vCzhiE`cYx?H59w$#s_SXkeVxT8RB5efg&*7cUOaI74j7+9BQrbgW+IKJ%wO3` z_GWiplwTM%j~*&}j|yhYw_E>@7n}K;SW8(@RKfJ(B4l5%b)}&fxF=L;7uU|r>uagz z_v{sL4+DSpBwpfj)aq504dz=>k(amhX-NnW>W%X z@0i%y9yn9wL3WHew*6MaKEXNao?MU6-~7#TG;@<7{bZ2*-XouhGCp4n>C%{fRt*ZNEh3@1_hjH!(Vc zC9H2GALu+sxYxOWF5=~QwbbSX%Jy_4<#r_0W&HN-B(3G=O2wo~(Ob98A=JuR&+s1s z%O%7OJDqA{Tey%mXnN-P7f*#Bp;tGx|;rU|l z@D+_hWr=@dAvJCt3L5WTOKBb^nS#eT;Je`T|EB-qn9E-#h{dymhHwgIhkJKJtR+h~FUfl#c)8+H54gO2@ z0AIDicwNx>pUY8>F513I_lgI)&Tl<@YR+dA11XW0&Ec2^l?_IfiOzZ#pXkmV#okW~ z7;fHt(7BsBOsc4l%j%#-crjAUwbAVK4vK|V6?Lf zC6WG~gO$BYc^n5lnfqQ|3<@r1ToyX^*N9j|Wiw6a;!OZb?C08d0@Nv@dhK-HMYgAZ-Gi|yZrlN#rv`FBTle;73jI63#AiyAhT2;Cwv8= zss-CW!Z~8w{*|(H64~Y%28c`hyJI)|V|~s%|MEv3>Y6r}7{Ve-wyBoaA^qS6E?v0z zlfdf;hyQp!jz#LC8bFas$bg&O`hAX@cTW)z=7nOQZ$iqx(_Pb!<1e?4Qe$#x3`)`k z#ZTXI!8^0RlD$ofCpZ6)HIdFfeUqur2>dL6k?Vnm^E=>N+p`#cmgcxla<*42;11>) zRQc8{d|NXn=HQ!e|1_{>zw6G!t1jwz&mCQ9KD?cutGISaZy;h7GFm&)I6Ud5;AbZD z`L$=upI6nycG8_OHbSrum%|*5LzUI{Rd)El?Q6>mq`_4`=rU8y_wHQGRQR5T;kN=k zz&so%UMJc|Rnn3B64}c>Q5e}Qe#5OxE~-j}D;t!mWe73^`u*PY#J*SP{Owvd?z$AS zQ)!bpeV*cS%&>Mj45`oRFOL#I*VK}nen6u z^t$u{2l^B>#VgTgJ3p?JPTRFKbN6rIh6SHI=Lm5OTD#`M+EI}K4>E!XPvH^r1t}?y zwqkZ@&3AcnoO!EmAXI++Y57Fal-6#+j1GF|@#id45bkNPq;tzHk`QXK&ZU;|;gFtp zJzj9RgVkbPZ*7e4Xbl=9M-cib)FHLJpSHB;?QQKt4B))^Dz&nB7L4N=WY55FFBODE z?w=P|Ey6yAQHPmDiAlHD;03ds%H$5EnmGI>zSb)o{kAy8zS~a9bt*nbI5X?}GJE=c zDP@0m+Dz%qb~*-4kcZMNG>Bd*iP9u8!ptmz-)3AlY_(8jp~Uxc*yCpUPGqhFkBW6d zl{bs3^#23+KnK5`9jl$6NcT_G?j5V1tR%h)9-VA-@AO1@AY^mr4c{Q)Q(EDD(~8y< zI5cDu5`#nl9j=H#2M93e-=>(lqlEl-c(%yC;_Ke0Uw$orC1hC_uNe7{j`9<}{40O> zV93F*K+BeQ;pJBO_Xsa8)t+6de!bQBe5ZEVw8QSj%S(Ov)iZtm)mLP%+R+A;I2d4x zdHfaPaHxbquq`o1hK6(mo9LCEeDHye&iGoA2f($o0klVcDdY>GXjeI12J#j8zo?;$ zms`FNzR(3=mmt$8Eq|JZ=Z$yMTjX=LZj&WjVQc$r5Au!UG8@hhQB*1E7e$LN1oLi%ah%4>W z4PCcJzo|;426IBphmkL4qkQL83C2BY58P6F<)2%jyW`4zWh@4@@xI*`yc98fAT{; zdH7K4VNLmij*d??0P*#%p4`_94sD;cjq0Nh_25T8(f5A+H}$K(^E>*T-}+7c&0qOt z{r)fiL=Pxq!;dui$Cv4MacvpzJNPFbex%?2+kaaRKKTVHR(i&l`xno)+ErGQa-#L- zUdkheEZ;OZ6b5U87HAQ)R;XVg(v(XP;=`oGkLu&NRjn5+yBrApN65eWTwnajpXg^l z`&0dg&%e;8{6O;+YsV$)mSZWD7s-`BULEx&HV zY|Ao~bIo(*tKbWcp!bwSNpzA( zswt`|%1~;#>DH`Sv*v%9)oa%DYPFF@Ls1eXN@7zIy%h+Epn)Yopa24e^3U7rj>(Am zeUT^cJ@>x*URAvU1d`KP`Q_fRW5K*05 z$@UW$fx~#f5An3u{K{w-12TU`ut?1_*>rXjEwtb+gla(psx=aha^UYgk z*!BRorRQlOwnYmE94%C#lb0odew3&sRG%TeZmQRO+0Q|FogZXxghp+P+{LdCQr}|A zi};Ip%ap!!>@!fe3itMpZ z+g#^Kc-$F%MJJzBJj|_=S+6;L-0R>azK)c5(SvoFdCLcCKCLzcX;prpVtgm@-%4J~ zn70f#KO&UwgX11b$I=dge8G@W*5X*DtMnny4x+eUg+9W8?% zvbaSGtrYkfS|aP(k#_m2cXRhZx54iILi1VCawc_9AM(UOuYL*iS3ti0Ma4q=+^@n0 zGw*fE`<)j1OYQH@bo167UE_M9;PJ`m!M{Yvx&S-4tyAmpw-wxdQ< z4#ejken_L~l!hj0)Jjc~+C~K^p=tw$ZG$B8c*QKe`Q+eQzfhuGW{QdMFiLFhV-3tZnv8 z8(_?6@c+gkA&E8xv4vwZlHv!9CH>i_tb+y7WfnRgG5GeH}gZufi@?N zW;-`Dj+wswo8QOR!Sk9jrWuM#)YXnA%I#RQ(&`Qe!CPrl=il{pdhA=iQx|PRV%Gow z5CBO;K~(~nev2M`-@A3;O^@lJa~pbe((0{e&goofvt4{WnSHACmlq@ytUSPtV}{m{~GXLmP{jFGvc03)&+|hkPjTtwQkDqn)Y~sZoMU+&wzYF$MWp`j*YJ&i9$wUFb;I!oem5c=8e<%g5d`Ob(e~w!eD;A zpb#Cx5U1>e@$@ofUvIsG{;+xUgsXhU7ZAYy40?`DO|QktY0-A0K=WzwM_<5s?vsI9 zRyeGf24#H!vmIf>a>MelACz}mYk4l$%iR9VqL9_+_q69b*=A7Q5OJy??eZ1W?Rs3l zX<>n~-9lY`8SJ6c>vjs{qP%sTzmDT_$1%%#mRDfMaguGoYX!%|aGf0oYxAZ?u4$$M zWt{Ik(}EAC8ToN!s$z3+D9>`8g|=sxq0T|SzzdifmJf8`3+0SDY2i%=WYUK7joyBt z{#;>Mq5mpHcc5!?7na#bsKnB=j)+hMP*O-BfN2jMyW&{t^(9`ttogTi)f#^u+k z`BLjDDg1h(1J0^Gl$Bqn(W>++ zkBCsjajb7_BZ3!js)#t!bKhI9(v{pi8r8iBxHSxUrs9f-W8+6$5l3Z6zUmzj5_hdo zM^_oYJs%N@=;5`09d+GhOboR8eVU8W z$4r03uZXB^J?UocC)(LE@;LLUU`8b=T;`<@Z?B@RqY zS!kM;dizrk>zls%X`SWQq9l*dP-32!f4e4Me;YUoS zYEoP0ElZN9y7yUvsz1~g(?XkyBtRQ^!UxG7!9)($ADOls2qU4Ni5I_6`5&3$>)`C_ zWnH^;Q5UaY*LCXmVQ^0Sx@c8$!d6lf4R+tyUB|)7#7W+1W0kFwywgVi{w_8Dw6tXd zPU=#Kwvc{a zGX&Mz<)m>a`tmVu(_np_x6^|Q>+*;}I+Qw{RxLOF^z38Oh^k#>>vfqgwQt=o%)4R= z==V~2#v`Q)I*@u7jJ^m|}<7h4>6`yoQWI3=h-QnIQ*hEkj=r^EK&;bsc60YW_QfI8R^U!75|Y)4uImI z*qqx|v%RU715sgZ6k|8DCR2%mJ|d`cEK&|7@>7u#u?d4%iVG3X{tk%46$&LbZUven zRIs&VL*{>2VxGp@VJ~-$1L0K;gg0)_w8K}wy}hh`aIjZo_{(9B%vZ!7d=$`}j{^G# zS$Fp594hy8bN_%tW~iZEUE3;i@N}KHTYV20`yEAeUdE#$T*5#6avq6#Al%y0#@2>X z%X*wbVlyv)88mGYF)zxi|J?Mf(=!_CAKsa950`4e_6JA#zdBcP`NF>$WX zKT(e$I-Un@Cyn?4!Uw#ETItEnR&UyD^u$6yF@H_+1hsQDV zv?0FXcn9HOtcVT?`9IZe2>B{J3YtC0ChZ$}Re(CVf<>N_g}FOntZt5*`gQQ}3tRe* zZ+M@cJinohSaK-JYWPanjK+!yKNL+l2yUL!<+bd^7T5sbKAp|39O9e5v2NBmX$n?e8iUGquiR zzK9>K3hSqcgV&lvR9QbaQ;o-}VU;8Wlc*4cLVC&mu3@dyaxUGvp~cHD>GBKD>*B?W zdhz--T{$?=4!S<<$>!U zGC<1;bg|64{zAT~$TJ*=p7w*;ylo8fnqJpkD<8FEDMvg4oq+abk=Y090?Uk{-axme z$ul36tDubIQ)A1Et5WA7cfQj>nc5H72qu=%SjTdx^Q@O@`OM-EEH|)|4D$m9J?uHp z_1MotihiIIP*KKl6uOS1sfa=GK@VgW*@92&$ZKiyE@K>x2#Kh z1#iEK)9~bS_puMBSFp~YD|9_?Obzoc?{=6UWS(osp#F*oMI51*_RnGxxM09LJ%FSm zXaZ2sgMgy7oay~h+jI|=2dQ}|W0M2_dTA}|fS6f}1`(^0(`a!Yl3VCENUvi40T3=z z>n)-SF$Z1Ak{h$iTUAo?&3pV9<_+_&8;7>Bu&T4rwjQOg%L5J$jru%#IR7a1Mz~;; zFJ7(#f)(Nw-W(Ue5+jgTy8cJ%GS2hnX}UN8Hr!>E7;umfGB7)nJBTj%U+K$oFxStOS;J+ zXIVqY?Yf7t>vs3h(Y4a4g$P&yq7O5)gD#@wOgGYgmvoy$-3`7D?i>{D%^U3-vqlFC zBWboUf^=xUOdxdi)^5e+TRXaV}rqoylY45Qlkt?gqLJ7MS9<8SAr{5 zP`z?Wy+Tn#Tc=aLK8#x_g=FO>qz1K`F0!mJM%*@&%m%kxWX<_9=mX(wnN^bOPpXGO z%25gfMG+w$+)*Je+p55K(po;`g zm5aJ|@scjzyrC=md%BKKd$e>OP5=FedD$_?5Df<3F!g-BpOMq@?Z3-p=8}2S0Zy;O za;HrV^DawNO%9Q|;UH(L!PM^gad-W3=o^}0fitML;7B{@C(F)B2$O! z9(*~C+TVM@{uTR7dxJI&NKB6bBJYjB|F60V+G&4XU-bvO!-2$u*$_H~Un-33zvODr zKSNsN9m<^t3U=f+7^^n84nyR*uEJN$vg~-*Gcl}lvd>7@_Odc3hKA$N+s%ZuQ?Xyz z4qOe4%gqxGm&17q=CdcN{Ay zpFsyqGxZKJ745V?RZhO^t7V91hn=G`w9oYs+fp&4d%B#@`Qd1;Zqd(^&VFGppFpvV z+Ahb!$a33id(d_LPAB3O)MJ|Pu`Z~DB(b`%##5%S$f;8CqM%Dy(+auBq$65-zGdUl zB$t-N!jM-na2$&5^QMjnaRX{Yr+~UBK)ESa?up)B*6H-#SNFeZqwa&k*mb_+ zV7R3XFS<50YCxi45pLh0QyJ#Xs{7?dJH5l3j6Rt-q>r$mIS*^uCazBXNlW!80>>WX?@ z>v&8U&QH;>=S~W2Bm?__BudDzU)Ck10fn(>Wa9m5DD+WY^-reX4Xs@fZc7%GDKS_` z))=~8$It0Zd`sP)poV~35qJ|?T{Rdh-h_0lz%=(z; zGkJs5@QZosW@7KA^ADd>Zm564ZuRUrHD}JLW0RLhQ*|RYgHWkuss?1N(vIu#PeK3y z5CBO;K~%NC&5KvBX-16pSx5bkw0I+xS`Il@!`4uDrN|N>Y7UuT6%mJk;ZDL?6?`uA z=bRJPvYjfAHnq1*x^!bt&%JzIFI>E#SFY{q>YatI-d^ZBpAc{D7wyagy+*r>&>bAS zwdi#1V5W~Xhzy+EG((NquE)Zh$Q>jP;c8bYEqJEL};>d1!9==?qznqS zMQ~idL6t70IyV`I1%y<6@>pk}Y00}PiqO;H73M4hW+SQu^>TB$l-a9vizQ(Kj2*9kZs$4*6W^f78@ktR zp2okLar^9UeXS^rsX;er1Lr+$G2Z+o?1^(*diu#n^z>ur^|psL_3kGw=<%(Q9v(@L zZnS!AbFB0HzVOgE>9MVe9^y;=*0@olOsr;F%n#`E1&5X*$864=lY+6PQ?5Y9xP#Om z8=%EN>f7Tc;a_1r2v8W)=eJsY|9Ag+z4ft2G`n?OiSG>vlq0CM$5^THXnXmX@eh-nyE&kZRHs0((aCSle9~PQx1wVeArG{|d`+xa98hk-{FrIM8fJ9a zu~}-#-0Y~YONAeY_VFEE{Lqyv(lri*moDqsFFsd)9J+Nr^6G z0^w_^@6U2LcZy^7IuH?x=;62<>oy|#yohjC#d^N`%)&wDG3>ryGWU%^zOFOy-UBXI z^UGz76g2IA5Dh+wytWBi&$FE&J@BUY1;O*gcpwbEKd24LSASVd3xQ$uFo^OoJdj_f z_+#6SgIp&tZ-~J;AVl4(bU5z3JS-a^@&frGu#>2(+FsF~&kFJane(HTVF1(40B&>5 zw+?U_xvn9vmeZjPm$x13nLjLF^$H74<`7d9ln?9(KdtX+V`}+8ogdDp-b(G;P{uNB zh;$H(nh#CZfrj&`Gb+DoI3x`i5I0OcKuqY+?BXoV-JpN8W5RU{{;bl6&@*qxPF7h_O#!~f=No*R{j+U8i zTF`O(w7P;tZGeQLe3?E7hzfBm_kpdy5k%Op-a2%$M-SwEa%Rr3GW^ z@Rx`B$I)2B9=dQ==lMd|_>YD1)Otq z?(l2>EKAXjl=wm*4g%hQc9A(ScFIBJ^3_wSuEWjL(xCThPB?WGA*Tv?YDOx}m`!KP zH;a)D*pS|)!ppY~^zyYGHg$V?nM2|$#`Rsja&u1?!KGXK@Vk1R4d9D6Z?Q4m)pa(| zcbIzztdYy6RmOIwqzvS`B%U}@ba-{Fuc-;OZHseRUT;UVaeH$^TR^P!6=fl@_8)?G zL6x&{3%(3CC6OPR&z9OdSg=v;I7sKZ3C;A$P$jkF!(sJG^*N>t$72jiLcPZzP=W? zuZ%%A;crXlB@P|_hnpThyQRm^PPG|xP4j^!d;vSh*THj4$aDOtaE7mhXE_X3jHGRb zTvL{^jbM}tTNxB$S{zd)=2DU6hB?HfH4u3TtdKQ$sF2qq(LbC1!GbZq+}qQm=g#S= zM;}(pnvf6nb?5qZEjSSF?d#DOOHG*ZJpJEc{F2eGea~<6QvHP z?<_UDIoH8%))l_c-{kN=i$-(i-2$IGJlLDfbnEsl{ld@xoIdyapOE%)=)0VC}jwTWBw}+E0y^DJi?-R`C(EMytxqedSR54?pflGyV?w17AI+T&1fA z5iPLyM`2(XnSrs6j?Jj$1Dt2x)L7@8J`Kf#cV)l4PRi=5{vg*$_;wlsoA-j)QYME+nuSR9h`h<_OHiF_e< zT8t6vn6I?T@!9pbP5crcmCq98Mfm{o;Rw0=xA0Oez4%u&gI~Q8>BO&XiOs-(2EEGD#^I3Izz-NH zv*2f$Hq>t$4R#vqKo85{qxly7dN8C9+z)~Mus`^gSZouc#Wd>bpg;0gL?|M@On%aF zST6LjJpK=Pm{q!R#F7aFTU5-$`F^8b_2rA|{H4BvA18UsgxvlW`qq5aL00Hj%O4z+ zq&;N?QZ%cm*mjli>KLf&#z*UvK?bG38@eMx5%-3TN1UEt^LPXpHd?UtcG`?-1R6y; zp*2SG&UG+aE z!@-O_s{9Axme?#6NphVafL2$5+6DJzuvu)|ruL`bHD|Uq_3(xBI&*j+Tu6Qg@TEeA z{|Qw8;}!mj!FvHySPm7wR_^T`=+5qecG!#_&?o*Xkt3`^JOoM)i84~+>9K4?V1qeX zeS)`4!&QW)j)oa*e>>v=AxdS|X&d=AGVcqLvgmHYS1O1umP5hQcOQQ^oJ0dgwb#txy6QNt%1()Lg&&u5 z1*4jog+4!AJpRZddWb{eV*h|VF{GE;=f{gX`%rc$qlX^T*|+g!@U8EZE<7rYHx#2W z`@{6|=8|6dJcqB_``RTh^P^4yN+Shxdcha-Tg=T5zW+D%tH1CI z`ki0uW9$imv!Y2p3%oY_*?qKfBB#;eenw_2fG|T zXUvru&L2oRpq)K(9LOlpY13)!IXOc{eOF(;tjkw!=+-=Q7#wRaT1qA@u|QxH3|9Yr z&5C5m z4-e3L*Ma?evbY{e>;l}Bcu3bG-Ff){q3R^{=Hbmdq2 zl|RfQA9xJ=EXrF&@704W;syoPh=s@1fI>=JtGAo&FRUgpvI6*;z7v%#TLmY5KnLsw^R5=Hos!v{} zq#aX14f7`<+E{?FEoiBfc0kK2UEl>WcI{hny@$NpU=TxetaloUZLP}5yxRn-{~Xo? z!eGJo0WP|^62B5RVB}x+XmiWqa0E5Rv}wwsJpt23nzGr%ofv z`sGj$i}(=y>L`9SOZb=QUy8_R*g}}52qk`zlivn<@T}><7Lh9L*yHW73d%S&%kwYM3|7 zA3}*HpzpB^>bO?OG$;>Oc_|d6EmzeetuM{xc;UpMu|ADt6~{hRG5W&c^8T@QO3-5pNuw@NtSr4bzy7P)kj7_}ORLK_clCk~gnaQjpdN4b3Y#n5OjaUPEz@>Jq7*D0qhWtlK_i|{b3-M7xw-(Y3HhMSk z=^7i^>rnsef&K!p7>&_q1#L%KVyDAC=R?U~H$bAU04fq$8dxRjReu%M*@pDl?QJ&B zXH{MELC|3BX9ukwY*L5A;Ifl45I=@u78rihr#4EC78Qutd_Z} zdMGUV;uoINFaGQ=>VN&8{+EiM`WHW`pZ)3opr84vf2V)*<3FJv|0n-c|MY+P-}F!a z=YOn!_b>mIKKbDf>)_T61wXnR?A}l+GhSB*6{+k?d9O#=Q_8y>2BG`6rQ0{PbM3lb ze&w=WxpqspSu19&X|tTzT_x5u5sj=IS%ug~5PQeV!GwOfZqMbrz&ibUWcF>)U*~BF z?JCERGK>EGP#7Ezh3PsDWxP0qjR%IyKPafTZoZ)pQh)Iwh_c~6}f#n&0 zs%%A_F7Q2EhABZA%a36B8mK!DsJhO})SXq3S6i+RAf(%~+?GH$ftLDO+TGBmk+O_2 zi^>>&Qfa3t{00lmRueML@NziI!hdcu(srA)WstYIf^{}|Ic%gN|4|eF9cyF4Y#&pX zH*<-F+kX^=_lg?&igyPKV8OQ{iYc*CO5WA5V9WGDL7iXDJAzlr3a8dnr`VpPCQGxaXp6JP2&Pf+}36Z8%+dWUj+$9O6bmea;(D^LQ|H z^gy62t-&^9`GjrPQL}lNzYC06vbgb!=Y!uvs~^z9TJ;gZ=Crq0pUwjbc|<6Jd_fI( z)iEzp4+v}zaj9{X$4(zh-E}#S0#(Pn_3~X>wsTxa8F%h6Au^}Kk2i+o!v;QiEGNc5 z!#dWmc8sB8Oh0nryq@Ii;P!=c(s(3q)WpUurA=bZ0Wer~_%fJf?I|Uux_M_Ve;vGZ zdr!L@re@KqGidH|k=3zA<;2WkqubH84EnExg}wmzVSfT^Lp)fn@RAfl?mFJ9zsyCi zw}rK~_J!Niw)i?$)IZufRmW!2|783@YUKmqZfUf~(P|f?a}I#rc%saKu#BMS6n56R z;9m|U{P#MUG8^|I{6j8@Y>};?z9sL0j31(14ei?I%iz?TXTCa!_nGQpa3=Xn#A09K zFc=&HG)Kn4PxDUVi{S2^UG41b*FQ44q$c58iBN$*YP0YO;6sfO`4K^iyWkgo_?<-R z=te#ytZ6t+*lQdgnA*q+$v&*=sXTJm6TuTXPg;R+?G7ZdN>`eT+=u3?PtK#Pi7~JP zwd{{0B0>?N6!aHgh#SVpV;9cq!kG=oW>t+D4?|%YwdJsQw8g z#(sFtNQwx?i|Q2-J+05evq!H%UI_)#{x0x^u?4W;xs@2t#+XW5oY>#r*Yz7W8LFLv zvAM$+?cLO>dE`l*fBLy(!qgrXGePFqF(vbGy3!=KCRC^ ze^FQYvbdyt0l{~~ymT2nijmYXZ&_zHH_vOf-|6`;yrkd$;P2`G_;>$FKlu|srGNi# zenvn0Gryo;{DoiA```b5ee@$A(ameu^wi^z>8sxTE~SUg)flQfP|EvC%N;eKUEYzA z7Pr*SZ>!n8uI05$+I{&Yz5M+1`r=Ek=y?u=mv-m6IV;KqU%Dg_i$Z^NiM?l{{q%)p zWauk-6)Jx<`0omy28#A;F>8j`FC+PJKXfd1#-QT~Yd9?ip%g%l+7gZsRKUDp{$407V5&CyHXQS3kmFaCGs!#=&6^sf)>G<4BoLLR5EH$Z zS!a;f_N{DKZd4gI>_cHusOgnYxuy%njf_gCns2|*Dfop@1B&I?vOL$aeVc|dRYtyC z7TsE((+0NsG-cq0%;`W+q|FPyKR|V`YrY_tZL$H8bs2BgGcW9{!#*0ljJi(bTVAS+ z_CkwouV{gabg3vRhv_~(Lar)v+OYirP8&mdpoK+`1)}>M!mr%HtI3 zA%4{hO-WB0X`=}sZ9)fZ`S3Sjl(YplIs9!QvyN#kmxglaw2d~6CSf~iLY>A4I%ZV0 zz=y)bPu{8eC?%SckcY=SR1XA7QAb25;&kXWT!GHLkZgCAZ{8Sm5x?r~e!dq(ESzUr zHz35sW59FEFhAh1PR2>kdkoR%)q`3P?)6dEQR}B9Q=6)aY`Z2IKM1&8Php;dK0^h+ z2YvIl@rGby*j~3mg}Q$l4PrsDH*gaHx3jj|EqB^>ZSp8B@~vi=H_RKs=G|%2P)H!^ zMu*W&UIJAHO-hpU;iJO(qQVCR#fICl>J3@a_gODMYz?y zl4A?2E7=<-uBO3E%h(!G#gw&VdpglOpMJZZcjKIUc7W&*LM%Zc8o7>E!(F?ER!HBvqo~xl2N}DQaM#pyLk5jNr>QkX>mfd zc12#}LJ^cM#|Eu`#nQ z7zbyMT+uv2;&{x>>XAXdQejuYj ziw}$5C?8NeUj}C!2J3-vcTWr6g&qC~zY7OW!6$rc){t-`BI+E4C5Ibl z5AQhSq;LcD>Q_W4;{F({B=oehYd(T#rH{T> zO+!N*qZwb@yf=8uQ;*C4n(rtTO~;90mDSDn>EES-*;CZPcWUVPn$8Oq(D~~#<3ELz z$TT8C5mBq@pOuwJc+46@KUs*_S)f!T#&f@i$=dQC5hrc?*RS8uOD|uPzhK|sfOl&hiv|MaKz3BKTc^i!YGA6~zvYgD|C zpNk?bmPK=ZESj@NnK9pH{9rR@ZJC~VNYkwgn)r{RYoa^53thjtr<*tTb@TSVcKGr* zXWaWAtrE5#ee@AETl`RUu&ccEe1pz*6!RVF;I;y2|EALZEj2)Ux3s)*RhOUtqMm#1 zIlatR!mIl;`A-t;=cJq)1^(cw`U4^T!hz8JA>wYizKoaW@Rtg9bFgJT6E~-`3jNh1 z6&8>Cp<669AhXz+H+2~=Jk=lz@UQ?4)N;e=`=J!vSvgi-Cl&hlv+cb5+hpN(WbT9e zm3RFG|HVtR;EU6TSjjwK3|CQg%pfRqf`BKQDP!5=8MY{pJ57Q+Q=&+$;Fcqx3dby? zN6Ph6cL3WjqzgPbSg{^8k)}AJEc=RqTDJ1|InY8sit@D{sQCglaGA2UhYe(dPQkW7 zO;g_K;(TPLf*0~i;SXiznNUs_sL&r!R=oZ3I5RRf>Ud+kW?uSzy2CW6vdsLn?@VJbd$~6)@YVqs(DPuN#Pqv*KxNhzs|2j zbZuU-{hV4Q-Y_|=i(zMey-iLVwGzgbdFPuNRsIGZwo$<=VHgZGUF*uU*=_CV-T65D z2I_XvqqyL?{B_Y6KKGBjSj6`av=He^j1|oUkLT0fo30 z);fy{F*SW~tZZZV!EGEbIr!9gok$HoTqV|?q;0;goj-F%U-eb*)niYx(Hs6j8NQ+^ z8U?G0*syi*nS+-3!U=D0Z?2awUgbb|Rd?n~HiwA=VWZ40w1&a!kDEb!*k@^gMdAwJ zb;&SKvfdOwY&AuxKRBR?b*->ZNi(8dmA*2=-(Nm;T9uKz+cUr3e} z2U>Cn?93nRYk9DzOj;ZW3Wvb`ojvVA=ktY@nNfq?CF3P4uqvK_ifCr##T9@F%1s8m z`||L;L|H`jww~d{K+S)t5OHKRneW}X%Y5Lu^P7~ER-i~qzTg7#KO z!Mn&Ewlkr*>Z<(U8;c@y>$i}_sbXsl}-2%q`v=k)AzFX-Zxt4bSNdiWjh z)S0)vLwfizX|$n5ZdID8+IFfjGZVL!FJIJ)AOEO+_qRTvkACuF`ok+P>-jR%rIEDT zB+VG_o~N0{b*a@XCHaqW@AD(h#oN2ObbC)X_LkaRh7L-rc}z9O`p&$lV?7EEtp_YF zb1EBcOhphed|PcBTFiF&NVubJ@3snuzp~3=5Sn&xD0Xg0H?C>t;wyUL+0W|>&whcr zZtG5#W`T3f2L012l>}vgvKPy#V_oTptAQeQ)*ybF9-b5T!>d(0l(pVze5TLW;rGk}|v(O_xW|peye1THo_Eg|QujoP0qgYu;D?ReF%2zTmv5Z)w zg~>d~0=o^<*M52;HaM0*FIV-!on%-Rlug9^2!#P_U$cVv2b0THA5uw9i$1Cg?JD1V zQ120>sx#;nQO>!hlJ%W$AMLBtwrf5d$1G>pM^M&@N?jktJr)Xn_i_}>({`<>^9)q0 zjbcBXUj?g2tSEjDSv4jkg7_M$v_k`jDxZ&dY})@G*uo7__HK}e+|~6c#KO>ung{3< zHaKouZ3poeNXXsxMpDb-JYs<}9kY2FN8*szu<at-Q~P--|6e%;1ruCjw*rL(4A;Cpz5a(nTV zP7jb6bS*{vrwS(gz%-pq_+@@ekG}P-8bAE7G~Mhm-cU|siLZi+krpu_&BjS!F;>|v z+PSu;+x&We;9vb&ALgtFK6qtZEARzbS3>o`1D}x@y>Efm3h301EQo-mq74WkzVq}W4_ae{|o>C z5CBO;K~%Tk5D5+zY^d4fV%4!)@@^3|1uzwtn_WovICO{Qp|Fa(Y>!Nqo&>q2v+5MyRuT#8@(|BD4}{P;DvI(trUC@}u`oX6-9z7=nT#EgKkmFwJQ-0c6iBq9 zk<_Tjf5*-B^}Y)>lyb;FaJ0~tiO~{*aaS~EoNZ9|)^cBGmb*H)+|k*^j?OH0wYk{m zOWJ|9M^nai)-%st)Tf@mtmk$+P2c=3oqOwhl^%JEG}@N1=`lMBaa_ZPA8p0 zY@FdB*yXxwEGoV&cE|Y?~RoBDOt3lA$`^3jL%n$yKoO>hKu6 z8=|*g9bOBD8rFG+kJgb`MRWujOR#G@p_VrUqag@Ju-O%Zr~W_U77Jw{Z!q(c0oNVE zMvcILH=u?5b!5^NWy!A~3l_m1OXSOLfz0w1SX4WM!)SK`OMK}niro@wVvf-s+R=p~ zhYByH9>lzp4)e*wJ<^1U0U%J8S(&e(efyT`XkX@&d+kY5e#OPB;^zp#gn&D zl@T%xlTxk-q$rHlQa9->JoA7FT6DNrDp(8*X=Fa&<@}6JWbd!x{=B8Gwg5upT=@ZN{V7M~J6`7r52;UvB#+M~4K&n5AP*f~ArWg_ zR7d+d7*oeJs9VO7vLXT`B{o6>8rF~jDYZ&bx77+kqfwI*h(jdOsg_U2>?j&V1vaqL z_#o7dC1s6SFd7z}!lrO@+{)|PoHgfNU-xx-+t+`i;-k>EzR?Y`99HtSS?w> zHqJe%-GhnR$>Y*;TbDn5QRUiP)3l*$x9(`hTC!mM;a`#GL1x}{ z$d>r9#JwfF(?W+V4%b;T){8mo!onV-6F8R0H=2~xgb~nCYC=s41?CNEqUJb6<2Hg| z%=XCWu7Ns&*rW!T!IzT&SxXhijdxKt*h4kBXoEgD!+6`G&!#xM0jAh*u|e;vWVbw5 z-&%YfrKbAVH2B=$S5rs}MAb>OKZ&ajh%5|9bF={GW z`qg}b`kfxDfl{ChRS}qqh@kutNGOP7icp1CG^DVDP()ZQvO|GmUcVEuNoB6vw_eiw z-t&ao*=@Ca#X1|Ia&VyF{XIE*R+EP=Xn{<88EZAs-tJPj_p>e^6n*;g4c%@h+Tlgs zfwmoKM1A4C6^9}BMKf+SnM`$NxQ~37uDi_JG-SOb1+=qmS#7l`L1;HhUTArRWBEosm*4G}M3dMZMy<0$pi z{1IFtQ@Y#}qWpk$t<1dXfJfyzXp!)609o94j0}dx9r83%OzeOLG?5+fmI=eBA&}eU zz|LKu2~}oF_0@0d!P}`rSmwHrSDkf=+(E6tmrBv^eIgS-D-SK?AM)r}m#S$e21)BK zWb!hLNgZ(r`itFkKS)fVvI0Ei7=Jb0P_B*85u_0lWXwPu zu}GT2l8-!8X;aG=Rf@lW=KOt zAJs94&jRL6Ve!FUubg*doCE`FUBaya4Sms&Z;aq;?327M^q^o1+(#|>BNmO8*=x9r zW9FC+=wmAUD6fj@vzhTtdSl4yIwLR-hmsL_O%!+4a$o=Jg@h*#C) z<(E9vxd+|%bMgA#!Ts{iU?EX zhxS=-;7x0q^3?{^=`R(i`0#cc4il97j~?>wjX1Hi^DEgx9H)Y0E&Y z%Lg74?n(m|Km2?>2$a`BvRYY_70Zk@S=F)3I4OUP>1x-ywn1+@7Cn3F(__y#Ez@$F zdo38oZ{O#xKVfo!(*qD&>4ed8xb_Sgz3=vc5{Hp-dc8^3cD`5>O4aH|vSksj)7(i| zejGsn3qkb0d1;V|tE>Pyyq$lTSTUA?LR$@_q^hN1jqBJhKD+@P^MB5|B61I1%Bvb*dY@;s$khUl&N6v z5bEWX@cz-=Ad9I=4cuf)_@HW{>LMx}{yQD8slR>uj&5JSrF}Nu%RM%?Gjza`!{Nc5 zxejh0C@+%k+&R#7;5O#UzSYDtl@0$@X$_Zn@f0AW`abS!w zI0|7quTTzcz+j6u)X8J{8W=l)!Xe0Vol&U{{jTXXS#?Xk5IO;<0YRx{NLFgQ2v6MQ zMEV{{)M|NXuVKa@Wrtje;W*K@#1Glq8%Z0@lB3>26W+nec~+-i3$aqcq7NXM3A0S< zz}~#*I=`Y^op)OB#eTt$9(CN%4Gr;`pt?Cu+MaOSKG>~)WO8}urk1y^tKrMoc{U*L zc=)Wo=Iw9N*T4H|eb;w%HIcebTu%N#iqMTVj34doz{WJJKC+3Vl40Ip%c3bU zXtB4ucc7cMZfiMTXq-cdblm3f#~duz@aM)Y>E<2j%1zB*ysYbAd|6jsx~!`ltZ&@j z*A71l?KA&o#D2*pscWM5k9Zd9l@jYm52uS>`&DSmN$o%|3=8ulPrZkFOpl`wF+DI` zS+z&yb(lfZaFWeLcHj$xD%W(U!l}H}bf#ucdYqtfwSiD(t`1jlI`Skxl(&rm!XB%p z0^7k?xwJw}^UI|8N_PmSK>(cuJ{DvJnpf2PfQ)?L#YA)3)H3sieJSJ(sQIh3c?Zp% z7KFWmHm3o|%m-;B5nZQ`Xkmcs5Dj|6S3Favt?#l%LuIB3yNpGKV3BFV;%T%gX~aM^ z#%-%{+j0;IwI~@)k}7bxu?+)0lHQ$iI*+)gVtR1c!r!-05J!>e1l~V}Z0P z%y-f%zH)yKeNP+vSoY&2lUJ&&Pv6DchWn1OPz>~4#whR=$OTTzmu;`=F3`K|JO4fq zWd<9^%X+M5u?|(x1uO@|SD^;9U_^iWkARNItS>x*KcRnmB#q&HFhnd<)F_3`+8?GE z(1TxeE%tPXgtH;BXbB(1xGS|@`!{8*Y>r3T7?1gK$k<6ziytG#NGny{hH;jDWZPX(}AulAT-F=5@Ud@NUTcMXkgd&d6OaK4#8LMWyUstRwGisXQGVlYf z@;?F(&z-@Ih}ZAkdU@L!(xw9}H-@z7kTx|2nfa4g&s*n@^ayZG43sgoB@ zMb76*UX{H@iiu;l%UTd|6t9RQ-g)bYILeDS;*aUz+WPnz@~Vuy>iQsLbVa{XNbo*H z?fV}&!S+*cdQ$Iw&$~6=++d*?Nu1UiHcX=>YYLmvWOR)Zzw~diff8%=Z8l>sUc96i zE?w5`{h2n;oWm@u2#Y{qAG4?})#bT5zK)doWr-Zh2SAx+rl6DMtFCAE*@-n+WDKIR z7XLx^<)E%-uv_^Mqai7$Mj8JH5GLK)-PdI{p$ChieKymxg*2N>2OJ3J&?WI(=0-dF zb6sJhdhzNt-QL;b%^M27q7~jYr6g&9f~r9QviaKS!|gK$i$Fh&YeA9&vrRY3ibm1#|CLp~Ia`9X5TK4mgW zns5^2w@LOYMQmxR{SrkqM+(ZS$Yara0CDy9$#5p9Y2?0L1}|0c&(x(WuvcG%DyRX{ z#XM$g8%d2*4*HLHX3A&V1~N+U1g3`6o74LB`uTO=gLmv>{NQqieZmMn5ktEuY8HMa z`c++MlExaPiJBOx%b`2`aBy{JN0*pyb9^iK?=p^mgN>GVc*_^Uw0~O{*rYtvE_FW5 zbhhm3p>|*IczCRD{i-MR-QWH`{e%DRNA)*@)hE-}o(k=(j(jPk#Q3dhXUOU0E)4yGh!ieg4w{3-krA2fPw(-^90b(_7UbAR}(o_qc!UE|Pq>*7uAzkG|s-z{~|-&THMN8O8jrF;(mKF>P!hcD{W&pxNm zUb>_g@7&al`HTZ=);uwnQt(wUR032V11}B8>%7z16g zI{I{3(QzQl$g?SftFYLM0qcTdzzM!bkq=~yQr!!Ie$esvf(KFJWpfPZSNS2$3Vb*X zvVZtmObz%O3&(jMZ_5n@gv<3-J)SN|v~ zv|&MRaWK)I3Uw?Soq_{hf;Vl*tI)C@3M-E+5r;Sd|B()t5&3eS=^?Tl+X13og%}`I zh8j)}w9r07P@!Atz>^Nj1hnRv61zhk>kS|;cdk3jwhuALpf2aS?hEW3&(9R=kv)bC z#t)N3fp6|N)>(~NDEzPSO;eIFp?@auV^bEs2^hhJsatONro_9nj z;#Hs!!%=iQeXL56sF4^K(m3^)FnBiFq|H%Vv1Qse{6cB}(FtipY+L+nfcQu_;gRq0Bp2aC0_oi8bPpM<3Bc4?iScIyAnMI!-jaS7SB)s%@{YdwUwQPtfk3 zE?>T)m#N%%2p1d(JKj54n6Il=Voby16Jf19S@jOA z7*3EajL4UHW`Ws5IIe~TrmqI;-48?gLC5KVcPPYqpYcCOl2KAy|1T0Z`RaF(P3hGe zJG#M_z&m^)+~qK6+~Eu1t(}E#-kEV2ysgV@WN+Nr(>{mmB^!Pp@Nywm#7)>PKt3Fp zRs?}cxIvw)@|l2mbG@Vw-6gq@4TQXh?r{~-B{eSso0aPy5ka1z6&?6hn(8Cy6?c}% zv%eDWoqhA_##Pbh&7(@>Ru?dGf~FC zNxslmf7HO9uTxbw>8p|8M;h8Qj<(w5x|9!&Q`EVG6$gs0J9PzRgw z(_p)WyuzdiA5M5-nb#UWqzkeN7AVt9nR4j+k`C=aUZJZ)AnI@^@EMq@eo0BR-w0Fs zx>6=%VpLdeG%Ma4>k0-&ODxT|=(dz?j6qiQbG6@Mv)Vu}$Tt$y6W?<4wXDnIV{4=$ zFTA5DOpYScW+ivN`9utw6{>9Di9>5XFVuiUY?2R-&>*Hk{s89%c>;y}Lf!!3ukdq7 z*S@L!<8jof=2@qrkY>S0Umk0Q`D%|B$~tx4A+Tke20B5kPTp09`AS!rgoQp_B?hw6 zvEBgD4;3x|U&je`d92mA!?*P17;6)-*(7bSpiLv7NfT^O;Q0|$E#-|q=2?mU2-+CP z!@OycT-5_XM^~cGAZa8XiLsH0b3+}CDgEm{9#hc<3*XkL(b>sZXGU$s_GqLH+A^i^ zB&0ED@wJuI*heTuX|X^^ezIWd4PJqrw1$YN7W*gcs%|*6{QN&bm)2OgKmH^|V|Y29 zY||h=N!Rt9H>B73(`3i;8EC9R4k>_hy2896W#$rGrJ1>uNmnpGZ0jWQntYufd^wFq zQ0Xco;)q{~6mcYrIO4^|N^*LD>NTY9rUmUIG^|Pq8<{mNHLY6KfvvM=^zdVkXf$ah z|G)FP1Ly%&Eu>%_=N;bje@nJ zy3HY>>*kta_W-)Au}muHC|Y716?%g^COfI~deZCm4teV445m!mGX5JK ztNs$~u`#~JLGW{5d|A)Fcv&xBxh1Es-=6Ez^<6#p@^yXo*_UFqMuY6YD|*zNIS1k_acXuMv<~Yg*-ufs9wRdMG9ZU{k^3jY zT8F|lrX2CH#uxOn6qPbdm>-2c&K?_hBlZ(5bQ85rWE<=mHqeR7we20GI`@Ua)?{EH?{=q-| zhx+S(|L^I~{FNWn6W{iYYM*|KbY?0|L*BR)?C7DhY96_u@zw@oF>8YTH@)la`tyI~ z`}Jr3+aJ`%J19dxE;x`D_U6ljeT_*ILh1G$UH{By^|9aius;5=-`D4!eO_1iQhtXx z9*j3Mn_zRgsSaA&iCR+%79#i{kPFQ_&39RYSZ_uf+qyz*KKjh(^sztqoc@scb?N0B zy8g;--FkUPSDw4BFMj4FedbeN)Mq~Tl0N&wCB4F@`zsi_)wVjAPBkBowP;!``RczU zj)nQ)YJ{KSB2qnvu<`?4%d7GeX$|8R`M{g~aw9XwUJsH7Wd#$`j`koD7*Z~*`D+_+ zbV`-yDi`U@;(xgDXE2}SwbDOH>&6)SiK+s6PdJdLX z{BKye8)$;|=m=8}ACByU+b4V~*a%Sj-V_MA@X2{bW|6C7g}g#uq5r%%;udk#3Pm+d zn~qUpM3Ci+X03Epsco!7ly!Qb=1)S@?fi-?9i>UwXP}CXJYz|$GwA9Uv?+_kl-f7= zb*u6?H34J%95F^(%K9+X&`!s&AzkA|+IE8&1b$XfhN4*0sYujyFGK{dE|*rlNMfTP zEAlU|{2*KVI?<1{KmBs%m;Nzh-iJc}V!MgbR@-QcuL9c~4!6fG2g8P6-hwZS(T_=d zO!#X*hT0NsPb`{2Z4se}yMa(f#NBxjy{y_?@mgK6qExGaALx+vCxs?jXheh}daTDJ zqL)RSpz70K={R_9*#}c2;>fRvo{zXUb(x{O=>W^uVfm}22OU#m&^K>5ZEBdWa3Xlr z1qWL1!qb4%oA)5&zc7zWrG46mLZ>tH+H@T!Wz`Ii2v-KfZT*N)M3Cz5h~s?3@pP}H zS|-AIr$O~PmP2#udV~K*6g97fz`7$gMWgXZ=g*(jV~; zq#~^b8~giu<%Q?<%;%ob%l^k2I8^4zlucDDamb;NsJ8kn6>N3dU(U4026oPSp>Dib zN5rPih)9zBmX+!YfigOA6+4W~F@#{h?~{QasPpd!>)j77JLI3_`?MXguWY3VMxnh~ zBMx<~_UGadxUb7McJ&em!WS;xgx=OG90)Jo*wZEC*Y50V-=|78$fX@~C~SFy_)U(z z+Q>c@j-iN2u(_6FwL<-xNW3z6+4}GS6!_$zYwbErGZ?D4pcWN1xq3tdT%QNSU4U;> zz%2ujH4QpIm68#(pn@NbTi$hBG~ry6be1w(>|drFekL4##vqX&24zH9dJ!(ek;0Em znZfPeK9wS@Jd{*_qi3cke%ca1(P7DC7Gl=nrDJB4+FwCkN66G!&NMiO7_@zpn6{MH&W>LE#3%I2zw%4^)epR1pLzZ{U7ydikKM(1 zswH;k{5Z5=N8ukZ7M)bPk9bFfB2LHs;~Mh8ba(GSONE*({3V8;`r`BY00+|F_}!1{ zKY!?>`ppl0O#kKgeqSH@=%@ADpLj+ee&$*I!AqC*g&TKti#G4D=FRG1aDty>)mUJ! zcpgDj!LvwWT=48APCIcQxSx$dWu5n&ar%!El6c7-11~pkUJqqV)`F}Dy@-AR9t?)- z&xmmV01yC4L_t*b3RCzP$hX`mwR{JAH^j@JcrnDRR$%X{9#~|=I6tFvgsi;s$291a z3Un(a^1)=S{3;L8(T(gVLI1k4p>nr_%`#(LJqrDwD`dF}0|8X~P|`9_ImaTP0zvx& zo2k+)rxWsoZUyBU_yH{r9p`i1$TyU4(KVW?eV9g_@uF%v!1@)Ty4W+ktVf9zcT(mu71&s*C?XU?f&v3SP-oRL&RfBhI&k24YqPWL>$E|;?;Q_&KBz)2W}oUrMh0wJ`S%zWw~S~ zOJ1`JYlQy@+GyNpYio*#!(g+N(p=HZRMG*5ynW4LSF>hcYWZTg*i*i7RWCpPMZI|O z72RP2xtvbajV8)zq~NO`j66hw4VK}rg8Li_cGv{h10kENY%jJATSep56O8(+7*gT`o!17Fg$J9E8!ZATYx?&-#U z)^68mo+jGQK}<&~90)sNRZ^>Hi5ryVTB>z~1dY;DIacrmnUUdhksP~l%p%lZ7OtU+ zo$hn}3?f+H6^go!2iY}^4d)S1osNt=E3y>SPeK(@P|ph9bKX~MV*fnv%13C^L)bjS zNnnG1pK@3jGY0$~664PMib~Os2npbd4lfTN5}4;nO{CG1Q3b;Tf!*m5%v333D-*L! zYznln6Y322TZuy(u&%vANCQDdj*9-UiP-dMUWT#Qz66X=L;!GcBuk)^rUz;%iyT-9=aSf*Kw4Loi`w8Hf_{<>xUJq#!_A17FQ` zm_}tNV_D_VHLdB&7iw{vJEgj^b(C5|m1SfrxI)^3px!|{*Fiv5_VOAnO4cK0);-B5 zK!S0hFgX836&zT#T+r8@*pA4D+-Vc_%iu_vLbpfyYe3=Y;hhe(LaY-8BJ2ShWs{ z3|W<)d_=^c;^A<}$GT!!PrOe4kEnV?DB?IA)re3sbUZKOcv@fK%j^HWT!@H!W!+PL zKYPwI?pb)1ahlF)vXwf-18W_K>dod>$X64qrWpUuvb`Z)dE^6i{s7Ac{Hi!bTo#g}#M_6_auBhx-E?JheVEHV!8rlQW05!xFzTK+mS@1z-+ zvngM&P-NtpuL(*?#X!9ns>^|U0q(Wm4-)tLS-+<}-f1w1 z4o5fn0b*mm)CQaO34F_s#ZAepmKOzf?s*ey=l3Yb)OuMja!$cgAz5ftF%P9U{`Hzc zf7PK)ko`_`jK~zKDppZ{1%jx8sDl0}q(;#jxjd%E97J$I3qF$*5^*Nfe3_UVjSd(i zx0qr#$h$n7>y?9j-9ouJb5>8i?H&5+um48<^}qdh^mqT!|DgZ=|M7p&5Zy<>^LZbZ_FY61x_fdWDSAJC={n$rz>Gn14jf!T| zRtMC(!=7V~n&-S&16tALU^)m@otC!2!EL`^<-V{0swLwD0JE(Xr@uyqL=|^kuWEULnu6>=$)4)IN%k zVOt%)O0|s47I}+KPsOy5{0~Rf19p~zJ}~NwkG=_zfan+dSl9&AHc*$SM=M`5eNo_z zOdl+XcUS3B*>cJk+Egf)57CiFzdKESrtO`vvSmH@L#>DQ73}1yhoepdafMNMtZt`i zBC%fy$g?aF54u-!IS{}v2+l^ULr{ihO;~WY__gcI##EauLSxc?ku?})60v;b3ja~f zmN8M|YTL9UfNdu0QfUGrPPH!hgjWv)>=&A2Kok)b4U1e0Z!~1odZD8beZK}|b)3;B zqHhwk;mhcRLR;fjXEr7}x3Qruz8a2su_IP*6GOx)(xA%QLYv3VrN!l@EsgE6|8g#J%xqE<_xa9dzA>0hWayC*ZhR$h^W3M`sy@9^!cdXbw~7h5j`EsNA&54(`mzxBi4=R^M`i1&pPJzT@{@WMD*n% zdb)1Q{9w;?C~E#R4EYhE0qc79$*^AoGV8DZe@s0(^)y3ShnTMgbc{j2<9cK{J%oM9 zoEb3K9H`}PgMBe|tOhu33_h>Z!aNw#E_*)=dIR-XiB-b10%wzg^(oXu$U0)4dEFdu zjJ0IWN1ND`E;VJYFAfg$xlesc-OjGcD=$lY`P0qoN_-J8#*2N84|b)?uc+Li9o{Q3nQ)|(I_7P|0Vy?2eM2gZH0Fy$8f|EgQ{nDxsX3di(vB2- zRV-}SI!QhhriK;A{-#hm1T8@(F2^a5Qykmn5KarZk?FrT0^7J3B0^0{@_#{$15Tju zH7CpMZPmp{xn=V`*;E;CXxU8IfHoD&G*aQqi4Vj}`X|`DCcto0&P5HePziFR2q^HF zi3kJhU(LmgM#OcL zS|y4k_A{JWG^VC8nf~iCeh=37WJqm`o?}Cue($(`!@gu}R?zkW9}$Y^QC(U+?7Pu4 zj05ys##s@xjyi|D`o|qPDEW_xUb=cqxA>BLdvB?iuinuuelU1x_ds9V-P7fUI-Yn; zZ~n%w)%X1H59x3H{lBlj_ILlT{__9jAL#43L z*!C{8cju;-2RqFDxwP1m4sJ{Pcci@o9lUf!SDtxZpZ~~b^&3C`0sYj!{#pI{2S1{h zZyxBSo4dMs`GyoDmEDZflN$x^MWsXiEoyr){|4q zL0a0Oj~5$Tx=ugdX3usVTxCCV16*f}UdMsm(O5GMeYv4MAaf98*vO->;VEo13b>?1 zzlkeADR{er)3~nUv9boQt;R|D$+T8>)@`oy5uu14RDF04I)?(7L1yalW^}}QfZs<0 z7R*hAOHM9JLejaw6^WgRI)aG1ueC8wA1V)3k)UAd^8* z#bg_hkQug7ft_G6G7>cLLT*5d+_XVfDY7B|kXmM-69go@38q})MOMM)AgN>008?Hu zL2n0CD_VKlAEc{av^ziof!=^be&wsY$h_9_ovt9imaR6k5;`?)YOLj3Uh}KGQ(B9S zdB-QY-j&?4L~KpHkvB$6kO5;BE9R6M$~5Q=NUjrE8`5w{G;Qb`BQa6JV4S2{S60}6 z;%6#&!4JL!I>H|2Aca^MREW&aBa{_uTF~p0E*FQor5Dy`&-eKV`XqI%@vzQ^9NJsw z>$pR}J5ctszA*4Nd0YUN3XM}7qQws%1l`WQeVKk3P%3Xb_V-j^yXFtnbw42bhEn*h zyR0OprMA2GMYIt}_z7rB^`(xdT*sc6Djzk5psO;CG^b6SUuOsM^God=)=t z(Z~J(01yC4L_t*Ufx|qQKHez8yKLA?_(0@)pyr)l>FVJ;qb4a+=^xX_(2jwwH^VV< z+Ii*&SoY;Yglqe9S--zs&qZU|e2IDeq#9~?!Qaa& z>U0k_Q42P1yz4TV?Ca%?^!#A1y<5B5y>?5n#|HA|j`GE8+JE5{EiYb?{QvS|qbfG4 zpZ?^h^u-sR*VQ|>wMTrGyi+n8YC)3^G}K#fD$FolBxP^HG*V_uK4%j)XYLyd+O>2( z@2HHdkS>(NWsy|9>?bj=Jdc_u!5!WAit&-g{kc~wzn+gfted5-r{;yj1!s{cN29Ky z)si-wU)FS^B^%d)x1RNT&|u2<9`=?914(#i33}L>;#Io@oeE=>2PLz5TB=`O-+6r@ zCyk+qP!MAb);oeUT(2XA#ClF061?dEsnF#R|y+MRLeZdi~%u6CJ4@Q$bZQH z+mihQi67lBoI9gOA9+X*ojs$o{9s@m{}l!QUHlQ{>$@dH{XlCMwuCoiQHkp1b~zo6 z2l?g?GtofAQ#!+eD6`MoyM0T`yh)!tyY z*5`yl&Y*86X(fIsBD%)pum9Od-4?105N-r=EZ}Ed{ zy!k1;=}&#DzU@bTNZse_(umYovIMOQ!fDLwn~59{L}_<(-xXMaII_0NA&Klu~?MnCuSzorlW z{^#`Um0Nmccdk!-?m2z%Lm$!2XTGTRpokNn%Cj%&ncx11e)&KCoPO~ae_0>?_$PGn z>Q(uVfoBJX!&XV5j(9p|8$uS0JkuWHT1k)x&IeQv486u{ z7&BI1=bbhNnW5}tTGPCs>o4##aX^$wayp(68;hv+AR(3YVf}y*5i<-y!19k zjmEr4d`Rwn&nlF2n?>7?V6O^p7m0SWMiJWp0;WfZ2z=@FtowQqk}v^c4H6XrRav2Z z_d`TfbqKy>_#(F{(Y6M--91`fC?SuFgw1*|mMBx?EBshPL0}LiqC*c-X7Gc+arZLR z6yc}jXGB!0)#bj56>m~`R|zU_;hzw=Jp+a&x&7uXH#G*C`7Z?`)~!ns$J6=>T*Fuy zs9hL%)01h-9t4?rejJ_pO-Vg^|}Y&2I@TXry(K^ z;~jB2Ka`7zfn6(;6iWSnZ1e-v6uJp`tn}cW35}=N6r`v$L;P z{EsgD!RK}7->?%{kior>-AjCP~jjW-dPEG!WYBuZ@+^RL#NvH zLsH;_G04Rg5sC;nDOUeUZVlf?rV~uI#ylAHi3k4W#GB6K(TlZoa>#X42}Z zcf46|d+*bF%RAqux4iW&dg9SX_3-u?ZL>)qQB7hTq}patJAzz@f%zUHMvhXb$XAUI zY?V>YGLkaP2XXTIdIX)Q)3hwKv7BjRzFRS!?P`m?)r39gh`2SxEuh}x%DfwCLo1XO zjWt@RcOA}JPkX&^Tn3r>0UgSh?(FKho4525hrb)`SUVgFsbW^Rr*x49|Nt!7UH}S<)yE~#A z6bE}cxcH*3eDY&@_BVe`zx#7Pt>652|4P67uYW>6|L=cFzxcC1tM|YEH}#QEd{Uq1 zEAD0b?-pOo_D54af9ZyP{^x#8Km9L$TEG4uenr3WZ+}id|5HD!U;X)C*U$aJuj&t8 zd`U0v?rOJ4^DK4vknm*^>=!t6abBa9xWr)bk4_y2)6CfQzeI~v4sk?4Pe(0$k4K@W zJ1X${WuBx+swu3App-^S{P4kWjxVN8XLd0`UZB$>C`bJ=RnHe4jXijAg)#7qMFGnY zf7$L~+_-|%_(3+%b-h7mx-MVmowtsZnE*XlwBgG-m?(uwYFXxGXg%{@bc{N2&=)#l zF{l(iR&cubkCiPm2fQAsBckM|T$G@J-Nd1-f(5MsKBzS;bVg#aGa7!$^r3Ey{)9TG zEO;AW%7mU$W`hZQj7K&h&+-js$z+wb&cKhWENg5#Dw=>X(Q2WIIvV7O7$n-3nD~iw zR5`v^ovfPYe9(YIzG1x}ZPdkORaWFXy$;(ooEI!!MxUQ$8q^iYdpe1^b0I90${zdWzwEW?jJm^G`jo!-AQyl(z;i80Iz&h* zq?S3|j9Q7ct)x~R8>riR3w{2JFY9+d{QLUk?|)V=eDRX@I1DcDEH%5jryHMpQJ?rP zAJhAP`8V~cPyeA_yn0i&7e)SGO}M;Y@k(+ZcnX6L%0j=6sZ0SePkrz`a7}i5&|WX=2>}9Veg}I-@0AMaAclSa5N+x(U(hub=>bj;$k-V8)2Q?PgOjl=(j6wtXD?mV z=Wp?&%M(xPP2c_Dp~AU%8?eKky-a?3aH{zx?lhnnT^c;7i~?*U$Xhf2&{p ziB7RrfBG`q>K+0N~8mgMD;zQ1Vg7sU@Dvo#Uz}_1Ka_I zyXv8{E*tV*FFkFH+)HNLL*6<+$V@%HOi#mlnRR|$Hx&kc@gRm~QwCUG!Sal}0I}$q zR=^MP#KdR}I^Wd)xJKPjlQ!QU`Z_gT?m-<#JaQJaX6>t8!!I~3pSas@u8~1{#Aojb zhejX#Hd*|(dAU?1sZ!Eg>U0LW%`XsJ(P^vFtjcSZ9%QCqyOB1C%mxExy26BEFfL+& zD!spM8}Px;?Q`40ZJ>STjTU|#4e_zOpvLHWJ z)H2mHuNh*XDyM8+z?LMJD)5F(wUXt93dqDV^9xx)K0xq^pK}5z&<8=Rf_el@2PD#N zOQq(4`9#}9JSu1tm1%&-N}W8kGVoW#NmZ9+)#Z@;UYnEZ4}Rp6;U_IzagciV60_Dd z^~42G4Ze+8M5aw*?FAePMdeU?$ErAO^G+(`gM+E&{Di#oDi%l(z; z&%(S*j5lvGCZk4%`Gy$D>jEY82Ch}LiU`HZPDHp_hiQw$kD_BT{V~7jy4udy+l(3u z3JnzYOL9kqBH~M8>x~#$e?eu|i}mz6@4PpP4*R{0PMPtZ8S6*1Q(>I<`5E3|5Uv{M zx#sfLISH$rCbRq`BBI7U;y7K`i-^Ny#EP%3m|wvY`A}<(L9dQAM4s2{7%~5okWe;r zAS|Oz&0?Y#uieo{pLtHd_MwmI1Hbo4{r1P6(eHljv-&T;^ZR=L2R@Q01yC4L_t&#Wr`3(V-56=`<}yLst6rj<%nJ5B7*W39M2zOLzVSFH|CJD!Q0XY zy@Uggy!9!)?OVP^n_u@XwQqe)6pqdCj zRpZrj@H)Es7~>1^>80>6j$dLNHI835(}aD`Xuhj<_qu!#eD{TkzV@*#z3co`k4;88 zL;e03(6VoGIYEop0m>I*=y`63T%T(&T%~ceHlRhHITpqsj|fEsUNKV0ALJI~gok|L zJM)&e>%HIk=k%RF`Zx4VKlZov_P_jt+We+(<`Br2z%34el{2Bd+fBfV6iT~$6)=xqI)4%>1{pVl#P5s`- z{y@(>|B7C`y{jwyU~@2T)opHQ$-#UVTi1!(HRj?S>YVY#a&-2*%5to0-8#K8X=FZ1{uE#$WzGaprk5PuPBuqK5SFi9R$`l5`tPD{eXWE zQ0z6`9#5yig@^HUGJlX;=VacHcAoWyw0Tx#0?Hs@6_qEVGgZ@BWkq466<=j_V_y`| zrbs^gBnEo}Qk4~%x7>%q7FlA#SIWWAe6aW@24s_$9K4ol*?5llnq`ccjK%~yK{jP{ zP1W(qztbiYbc0^oq(`>s5vNVJEJLqS^XOT=fo#)yD{L_+jcpdsZ5FF77K67?gAk@Pv?Q&ZZ?WpY~ooLTG8mCun8{~nk2LpECS8=TIL?#272cA*mi_Zj{ zyYGWSea;K0%T*rVM0~{JB`p-eA^{bPUje2N%SRiSiVXxsZaBkg&&MT zlL1vmVxX|9{0TkFjfk3yPEp`9Lquc7!bUsNlrMum3?_VaTjjnADS{Nf!D#iiweS#; zqVf?*5dj8jet>1hkbZqMRl7kwk|0VDWHNZk%XQO^7{EC1`Bp)@qM-+CIUccsd=(x= zw2TM0Oh&~;0oF3-C!B$7~cGt~BL`_EYv?l&5JUs7> zp(^u+UsR?8PNoO>gHW$Yg?a2XCNsA)^L&60drRI^rv7T^HEDqLMSNVR{Tbl&0Jp=u zsbStQKOmxyaYSG)syHGlAJNk)OZ~zxAgCS>z6+CVSVHepJQ{l ztLyt&2O4Y1L2?lz6_D5vwyZhhmUV`AhCqoL)lJwj0{fMc4nSBXWUj+>GHx~Tnmlebw$3Gwv3+W_lv~A?_Yz?6wU1+ zuA<6oxSY8jw3R$81Evjom9253Zuge_$_e&T|j*xJy! z@womev4;L?=*w_?DO*Svd~)Ars8Q4Kr%|r~C*|U@3_@39iLw!)hIeT@8tYx}`5Jxa z_xvUOslV_8+WP9Rl^%IaI(tqX@8^C0J>BV+G}=IaqER!JIN-?gOIM^9Ue^8>n1>(x zef`!i{fhqWzxYZ0v;Xy<=>Pk_|1ZfZWB=%5*C!69(?U&hSY3mq^Y%VeT%gZ#-zv*}nnjGfyY`Va8u zHoxfHo=N@}bLWLvN2>y|60Rm?#zbZ;cA)a4ONFH1hWFTdAP`4xYQm0fMUm4z0zTv` zSgb1)j3IQ?gJ3%QKA^v%)cpQgS!@W{7F1l$d;o-tG9ElW?g!Kk$Id!`vdn4Y&pPX6 z)_LchM4~3*LThjghLE|4DvcUAAto@Zd{PuaAI7n2&|HUK0TpzDX#;hBYf7;uivwU$ zVnEe{6Te0@e7$P;iq&%H7_s>@#!Siy^>4S0wiryC^ztTyXBy&QnB_kTI^nP~VbV_U zZHlh-jmje%_%S+`x$K1U8`#;fJr=7Cu*u}Chd~DV1dM50JrHKqg(`8-Yp|C^ztL-YoFZ)|z zehpYI#7$7u>-YoBV}kYcF+NZ;Ag}Aj$Z5qaGiX>fS9$jlel|!ZjEN}=whuvxu_D?~ z7@LRfNIl1}rUX0&>MveUidR{+P1%T~h&YCXtlG8m1A?Xrqly%jniqUTs_9@X8-iAZ z=BE)JKKkj?e5%X~}8fh{H(5h|KzR+FIh)~2SP$&VdT@i7Z zia6pAg(sL-*Emt&k8tv|wyewOf-kH05$m-{5y#W|uXuUAScj{=UwS?7_3}Dw=FuP? zY`EUe2SSip$IY7#`KBk+mfZ`scM=hyh^XR-xF=r~ctixxqdSp~h&q=dqL#PJbIVkx zT{Jb2`*jeDPJSmxG_2~OIrJcnbuix0-egl37e$}ttH6g}yr>VqbWxwUbX9+F?UugC z7lP}258}Iva=Qum>6LN?S>TP=%j8OUKZy5>QFw=RNvi9lLSA<=J}+2XXS`!)910i2 zIji7{cv?rC4RBcf<=FPz(Q{q4&L3&qpIa}p&JU&5W$V223lpYDy$*5 zv?3|9Z#aSvhgGi#;)i-YY|?W0tiyM#WkA>nVi@oRe&T~58}*3~gXW=rXN_3%8#*8n zU&CDySTux8T#-n@M+7M$jtX&E6cKP`B`cNZu}Yhh5;h8L_B_BPekg2pX0o9RTW9p} zg@?3t?u;IK%ahU*90t#h)jhH;J$6nyzpc&lXXSQ!f0ft|hTp&Re*yjY3G#Ia`LY|P zryy1`(`pyLMvSE8-feBLr}?V4KCW+n?>qFaN6+Z-Nzs#=Nspb`z~3>4QmH||q2Fi{ zbrxL6Dj~VbY-*6B3e~GHb_ZB6@B35%bMla6}G~-}-aPESZ4?V8@@RRD!KCb!XA?=lI?aZ0qi>dsB z&dpiY&O(~yB&CU3;BN$rgN5eHg${_}O+G4ID>L07<7QcECuc3ARiq8dOr>V3GMdVN zPw#;EESjXm*WWG@dZ8SusPTjVyg)<0AE+X@?+wT6UI-Pf!+Lc-hSUMqVVOY^Qe>HR zJXZ(aY4a$MUnCD?LDgSFUZ3(4Yh(wtJN4znFpgZuY5X8FUDt7X9qW1P{JM_$)G(lX z@jCzE;VMr4cKTTAK;j~+6a-CD@N2`f&B*CM!Vu$WnN2wdK%z#vyzQc(TENouP|Xi z9)b>@5Ur8hfc?Yv!Z&ORoVLBj=~bN#x&spYA@GH1jm5(FIs(-b)3SnUzR-;|2-_qn zM}CpB(SxpTcbr+mfleWB1?m^R)fWvmf;RhwVlY3^%l!+6w)=$=uB5`&sHT=Fsy3>f ztXR=1!@kQ9G7Tt{=}RAf7Ie#D9yZA?$RHZ1dC>?@IKzCE9ebd#N|!?g%M9Utk!X+B zu2S{vDGQtqdqD-KGw7*Q7X?&Uf5lRvY6qYFLTJz%rKFZbBQE5H@mruiSd)ENi#-{D z=Gt9WJl5^wfGQpGYT|(&eVwJcO*5)Ly`fS`ed1^Yng(r=VcvkxWEGGpVOo$E(j@EM zrUv_=V4mCk5$Q?WYTAzEO-LdhhY_ZK9d(y?-Nhd9LeL5k*g=+{Mg-}iyDp^1WK|_Z zAN4qv`34f0q;$0|w~fT=?q#b)t=GJzJ>1Fxy-&UTb@Dh+Uq;lx2%!p|)sev(qSft~ zu2{6!>OsHSfLiL3gWliUxc=R$q9~jheciBRXNAR4h zZDXvNH=G_Y^u+6>eR+?3qtK}1a6KJgtvc5J`h}3SurS7t;0=^`nGt;FV!J zQS)2YuSqx4R=20K%R4%^xUI8`9i3yNx6OvR$#X5(0I~(ud^FK=x~;r*UbFGG4#rzr zZk|^@b3wDsZQbEZ!cE4D|5#Z!9!sMUo3KQkMgF4dFRM%5@3^Plv%syu_rS$K2Ei+F z0L?UR!TK`iP~gMi5_yhL1$;4IR{Bn(Un&AS*fU>xD)=5z>l6CM@suS!`X_dcUWwd=(x&sDTEQmG%)us1l44VBW4LON<;5|pe zsg`xPA@(DFpc}P~+9uSd((iwi+0%7ButOFU3k+|(s{FKmNb;AOF+O>qEcyVZCzox`G4d_G6E0 z^w2}h(wB@f000mGNkltw8UOY{5N7uZE&Cqei)~iIuAg`m?mT8ajOo#Fz;u4&~bM~J5plJ$f=Qk+~Id7 zqL5S~=onB!^*<;=VwR8_p+e3&L|QAobsEU-M#5I8>QT;iRIlD{WluZ`X-uh7%82$F zrSgF-6zW~-vkJn!Svh&%$nx7$#zsz^N@4`V<4i3R*h*B=L(I8CEg5uR2^MO~LSx3o zly}U=a;`0~38vkG@!Me`W2-;`ec3SnjNnXD>j|<~ttjLfZnfY|*Rd{J=dI&%hFh0f z3g3+mXFIi2oOnUB9N4Pkq&FDgTgo!FamMK>zwAvGOO5677;BK}>n~hZe{%jOYXt)n>BYYiu5d{9DoQYo* z-ME53ZjdLRso{nOWksG5_z-kh0!=DvTB%K{ZEShrw{9mGF~Iq)gZh)sj9QtWQ16J2 za~vwABU>#wSZ4fApwT!I=^$=}zA7sCW`80=5h2f~##B;!gFNwlK{ z4Y5fQ3yBd~SeGI~z6w4XHAQ^N8l<8`gBvd<4E9W$3i>WL!0AF6n>&uEbwrp_e=QI^ z!!E1fC+1e+BF^+{HW-_^6cHv6geO58EoqOjpxjbHuSu+hE1=IYd*c@lxXp-MBp_b80T3WAwI0hm-oJP;C=+lnGn z_py;e*y|zq+Hor(RY~vx`aC>^FDa=cN(WXML$N~cih|fRq(@4OGf`J)g6;-mXWWLy zt;D-sO>&*+2OyE!rf)GZ%EUrkQ4vZojif}qO(YpfAtQ8jIo}BMB83dx(Ew{w9X{$* zG|}6>7ah@#5w@EY_&)$Ig1Uh4$)H8jitlNX4%2$Tt1zA&nT|NEBP%nujG>){8V0~} zhh+ck28brKj?^Yqq@;#<*tV@&`l~hHv>K1b9O%ayAsaO#*1`s$lUn+sQ6g>wLQ&IA zG&RpOaLoT`h}W0EF$1TyP6yud)8I9&!S;XzpCqM9Ln)-TNg$LORRk|yMW@0{HiF3M zJ}9gU_kx)Bp35CNKD-Rn{DPOVX`vAn5_O7Zlp`P^B=TmU!`6cLq+vT5pZapS_Hj@? z(XIw~Z648}mRP6Kils;59Xg6o3o>iVf(>EEhHY6I>^CAqbd&t<8ewcwMw-S{t&$pS z5+?Z|;Cyx;@+NL+Th!4xpE)??y?cIfM_;v-^#kv`puhQzZ_y9D=P|v#+0)kEWj!=n z>MXy&7UH|0OdfBljCjj~oHqG_(CUD%81ppJk~K1KZE7(cYt}^V4I-n=n%2=rT`H=F zJI2N`CuQ1FKrBV~p~?7O0^xt*y|dswbFf(GVA*MgugjEFM$}_JKyez~4&7x}SlTo4+z zAAXzykB>~OgLhDBlQ5@__U9!ANqD|)OsDjDC`Hkf{m8k^4L!VlMo;kN=Pi#vS?l}y zZ~hkj&=3Drec$(fpT6yzzESUc=hJ$euWwUzdgA;Uof)^<95uSIwXJg;=BH__v<#&# zDQG~#;e@|o$aRaWr*1>;vNAERK|tInvEH|gH)7k+&$C_mujOoc&Rls#=QY<`9^PbZ z&h*e@kE%KMu(a`zc7wwQU#S+ZcY9MG|I|nI@~vyyC;plB$nR*;?qpC;@S?Y03IC#2 z-bs}b;OZy)uSui%Y)?zZ&c=9y_tJ*aB56$fw()biJkY~@4g9m;`ab>HZ~q!32fDL+ zQ)f9eoyWH>I_>P=*8Xh2T8t7Zw6)-q|Mg1e2cvrg5)*aFzIMhupWz2X*`C(%ltPqe zT!)d#?J5Yccn!*bx+d8MXsENWH$Ir{=+1ISY8KM=gqVi>_Wh*>vnq-l$fQbQ|7B{V^2L@Px57PNIAALpghl09)l=o{u} znJv&JM!fJ3*uOUvYT8Vxr8@S=LA{g7rpEL|H3^9(~qgfov$NtLt0gODJeEbgr(MD>qAd9^+80ufayEq!DynLO!?n?2s1wO3PiP z?vBRYuD0g;dWbRgW_5a7Dtdx7dSmyN3g{SrnbqzJ>_@oM|30TqojP^uRJc`Lv$OL(X}%YltA!j?(hSadybkD( znuZxZ8E*nVIzI40_~ZZx-4+d9Z)9ku_s4}g03BQ=SZHeeE%t2_l*2K!SAgI6;J<1* zlS1~#2h$F?twh}&9B_uWY}fT9d<%mZ1oZ^<6dFlHOxS}k9{ib$Y`~g zD58^`KI+%9j&ubTjD&6w0EIK|_&-1{oqPtAmP{a*svSsM%rB zAFL82+jSq3LIyXHWnyXB7O5~uId6=&_%Wv6e~@ZHVFYC~A$6578M`A^*VEgp`5T)D}{MV+}s$f=Xg@6a}r9)S`y>tPdN6 zTO-E6DnBeQ^TYBAo6j{iw?o=7%+eU+V+u5+a{=Z^3w{epIxa1qko93&ijUa|!@Mgt z54!#~?SS}{pVm%mzvb_*N)`7PP9)B2*l&$NCJu>s57^ur{ZzAI4h@)NvmA`)1~c;E zu;$<-^w!5&$JRFV!k4{VPkHupbj7ott*c)8QoV$O;gg^AM4hpJuja9J-(7d{BOd!0 z`pW+=zQgz2qtV)0u{+>hmxziZxz&nH1as@59Rv}f1jG~0B4&<{%LZfWH2?KaHCn#s z4sGvCmtTCoPCsK0H&AQZd)_$`w}a}-V*`wytJltXXcVW|NT;U4`~=5Ulz$T3R3J^IyDPhmIXlN}+yA>N5sw4&@c|reZ&tG@`Dd!~rQZ zR;paD#Bc-w!zwU|2a0N#uU-$>vg0Xv_{=xuLYOCxl*jIfi<8y0RsE;`@~iq^Z~R~S zY2M8EeNzpdZ-ufMMU_1phb?|bKa^~sNaO1Iv6rw$%n)=>^*%T&I` zI2g^$YQ3to0fq(@cutNsX=qHpHC56$sqkakIkwx-1}YwtL>t=u1O6ei&s3u}rFK6_ z0qc-0SZrn}EuFN{x|!ZWTVkFY0SyeLDYUYLrbmzwm<+XLPv9vRXbRX)fqBDu*q4%y zN09CSd)%j!w=}S$sq(gO^~fs2EKR7rbBw&shEz&f<}ovf#MlX}WM%h12*z1eSBgrU zCx>O-o}(Z3ZClVjZpasjcRqAT`v-lUj@}u>@T?f=9F27j{C?;z#_s~Q`n;DCe6U8i zekvipDn6Kr5;+{5&@SE4qCqxOoSOKA21mPEXJlY80s*PIOv6D zsCZxywLyV6xn?=PkG#V`4Z33hMbCfj)0nTMg&wFwi(Ea=_i7G=mF9YtW(e9$oiw12 zX9#K=h>^kp;Z$szCN!8>fxkh}3_*>cUXdsAOg-dGgLcY5&$58F`kBsb)c%T-Rndm_ zpeVVsi%(3lK?Lcbtt#lD7cAxiVV_tugrAAdOw=Dx%LShe(%C>av(Q*3`+!d#NQSUk zsO6yW6PbCV=uFCp{-IkS9*iwcOlNd53!Q-}J zWYCGe#YXsR3MZzgvi_1;*VN&`n=U128beaa?WL$`#Y?2yoK-BaN!FyjRHY0JEYSDnmiF(p+T{;tgsb)hEtT)*jv?Z}27^D&J&2`ObsCm+}%&3~a zPpTBe-us}j+n#8l&3O(642TFN5E0DJO1(O%eYcG@KR3`Br|;F-XPvHndw1*nbI#G; z-Mck6Gbo!r0zp|7X!6*vEXE}Evi#<3f!0Lh;gg1ahQkEW28X~EZatRPH?%Z46s}`> z7)hg$8rRPt@sB5L2o%HxDH_E);bGyW3~fJzOh#K)#ReA^Y-R&q{BQ}~})VWt& zp{Kp*#k%4}FVe2dE|X_MNs@`$+HEzC+vR=e2!qMhi224cK?3 zvRB!laSgx`0b(mtW5TBtBK(R#ZAzXQrx|~n^)*$aq3YqP=IgBU&)Kgt&OKfIU5i>g z=L~IoFMJr1|x_A@xy-*Lk&y5rW{b#!@I%N&kI)HhC&Z zP@z*dQjhW52Ll$R{%WJaT2>7bzg&4vj2S0G*6)patTk;YGcQ=))oiS5c=(v+*2cPM z+g@Gu$V>IS$30HZe9R?!8n^mSJ^Kt@dHQL3>S;UmgvB{MdeGB8&K8TjuM%TzMEye? z9GAsE6rAjXG%l7;l(6#32w|X$!FnxFq|cC7ANMYV@6rCmNQv>Kpc=B zkk>=jqkJEEk7?{{(Z16hj0Vt><_dbi0%Z86(0{#P4*fzo^vzXC#tai}CQ0Q64*NAa z6*g*MY6OR=z;;Jdm$qyI5yYTO2holopu*QMrugKvb*U@Rt4Mcnz4oO?JNx)EAPz>K zbPpSK(H|?(zM$>|Dq>K0XtG`W(**4SaD=6?%(rOZx6oiS7@$fNa>i-BE5lUb) zHtW%U&`pW)ZY4F z2QC&R4iQNa=96Ifm=E|=c)s|+6Z_cIC10m{+PsKRL@1?-K06+X{!5Wmr4xakW+C|jT5^Qn@D`|k?ollt63HsJq`d>Vl5wUsNxXiL*e#q^O~C-$OpkbV`Iz# zY{&tm;V>d%Q%tbpdB5Ra$k@)JZv4zZO5`Tg0(=>tEQRd>Z**3NV;y7Tz64$3kThh| zJFIGrh|QRBn%yAUBHEvMzX~tz54F$4HL56Dje=4FP}<{k*H@-4)X@`prymY%&p5Gc zL@0qiZL3vr|4JH1Yh=ADacHYG!;fu)!9aabb3<7BuY8_ymd=0N%XVqo4$W`hsUG_4+#s&5tmxhYckAA}?$n6G+rn&Ly(()oTv7JEg=pv@ zK5mcU5fF@-)q4-9&jIYRN1mrEu6UGUCbU#FI`0WrNc&HddbL)$y{o)OSzT8?a!j{; z;YQtk=K-y9@ZG?NaTW4`aLB&Ne=X0q%sIKAvI>un*}YgMJRH=A#KeU{L%cHklYXyq zRT{1@tH~SMwP%N3^pdN!@AUoRG|dk$1Nks`@1Z5#aQof5{ooM|{r;Mrr5tmMLzh;T zHN!!0k;CO8@mav1nw!Nj2i`7TSOI$zQahs806!Ler09W;#w$bEZgTKm@_CQm&<&vdlGDSF7SM(gVH66*6S z%K(_4VWQ6PYs(A=#2F4s0~U3o&tb~wL2avIGS&qnI$%WOhf1UiC>z+*1mSeSh!>_Y zmS|7*!4MkM5%}I7{u-HgzK~~ZiaMj{x$KB~Myl|wBEP~$OTD?6P+w+o*+Qd^cV4SA zp2)hqMr~fBjEzj)MuSb;C5Q|a4;K)CvEck^2pLM{6MRpDWHDix`ICWhJZ0fz8V^m>EqaMYe0=(??rU8~#K?Cf{6lmGRkZiTzIfOfa7 z2nCz7mX{)&4t0;?m`GMBagd1eBJI_cYBp>Y8GKMBwjq|sC_G=qj;RrW6(BiZfA?(0 zfw*i&!+aZ`Tx1=O9CmB|kk~9=6z}&H5lZea_YtV;n$1y9y$YDGYWlL8V8SDF8BJ_w z=1oDvi~z!c0L~9|QX~b`{DXyUxQ;D^Y^O>|iGgH!HK8h&_| z1}`3d1QPp(n!VoMxxOwrd%vFWm`CXBbM{GdNwIrT3y(M-enxB5GUVV_bI2EmokO?Y zse5lffG;DB=<`vp(wKM1Fr<;P#tgn(NgmrRq)c7`wpx$d@4D`5gM)9wja!Y*`sxyg z&tp21Tgn%{_=URU=}%Fbn~|!%=69T?mDRDXyYUWv_U1eEi7(!ydzV&pbahoJg%;;$ zw0mJz`xa+)&h~ko!!6C8UT9upB@TrR@H`4NtrzrTqf9RTG4Gi=Dj?T1PsSL_j5hch z0^cBxX-MWkXY^(lv_WH+HX6o!a6lbsgEp;1rDGUbt!u5MTFXqpWvX3g9yI-dhRlm~ zWKAR$FmFt|I-BB~)Zl|?n@DH;@+T=|;JQB`qzsU2@?`i-nMtzEwyYBr>M~|bA1++} z;i0SX;gwctP0(*GRG!SYe`siuuuo_AkoVXO_@=a{tUjTw86-7%)dVhEd_Y%} zE%lL?Pk<SQ8%!SXGF&Z_tK^`ee@IA>F zc2Q!qOk}i0ECwktE8rWnA*e8{C<7zL7NT5e%afdZy0ob=5pR*>gn3K;Ft}oBbA&r( zoZL{*4kOV9BcS5I%}`gLN!;V5RC)0vMsDM&tYBu66$%#Q2-Am$iJ|{vBqFFgA(wlTm+pKbYVzJ_+UhYsv!4aqDn~>8>6~P{Geb|s+fmWNt;j9WrR{nqBd?tg#3@(7)gnkm02#^G{}}=UX!PgDBlnJDYOw8Xl=Zr-i|r#z4#)D1MQH(*Wgzl zUwa(dnnOo*x+*F=^)@gd?^RCj1Uil)4^q5V#qSD6Fx^BGrP8~S1qGonc!~U!iJ~&qF zwRQG+XX&X|JxR}_>@%Ktg)Tq$Or6WYeD9#=GcFes49ISMkbRUIVvymR2z=87Gyo?( zzB05N0!jG4ksXuHsbVfAHPE#Y?M;3Bn3sC9<;M`;j4z`KDT8k9*)=7cN^(o`VBFA1aN8)fHVvrzbs3f>58n|6qauJsIzDx=e(P1`OD4mK=r zWlnD)c|b6tydh~1nQdMKgQQB-LM9)Vj|qj8Ph`n{-4B$qe>g-xR@DT4SqqzcV;thezicDs(SZA_IUe4*(7 z?fno8wb+V98+XT~l{w$D#d1rXo`z)|PLxxIG48Gs^&~bRM#ToKVpmu*=6ldSzi{?A z{Po~{ATaGYUDv8onxKMKO7xpirNjZEVk27@)Y$By|4>k)U!|lpg(}4)9n_aZb)=)M z*LoHDp;it>z3|q1FmNPK1&?cEI=^9~`9R2yFVN(Pz$y5Mz^{O7KM`BJB@t~p!Za7% zprgjz25NmpJNlsu_U_V4pZiQ*e&P9Aq^}a2$D}0pUrwx|RhArp)?l&0mxx+vg0p7( zJ=ILY;2r1xwrYlhK(CU&aafAzTe<*xW;%J5Bi$fzgIgSd7&w>udn^qZ_x#h zd5pAkr&hSFSOuy=j@gjCTc$6itowoZb~2CsjBi=Zae9QTjH^p8I$w`D{~Yb!zgrAZ z`e{|Y-P^ST--k1O`cbs6(k!L5w4|lG4(Kx<`=kyYUXuFrTBDD-9Mh0~8aC7#QZUa+ zs__riLPi_2K^{p2K}xF7?+>$PvC+3K7Yh5&I89%{&ERvcey-B41$KC2rG8%Kbj{s2v@SjGY+bZ# zr_P#N(2lytJ};>%4hXxsAT!DA>olyABNUNfLxN^VF0@h;p)p`OtHEOCK$rs`n;;%p z8=H~ys7)VP)VjjzlElr zP$rd29t@wBZ{;K<>axrrQS|k2AN&;YiwMk~9ACc)h-+J@namyLRbmmtUqAJmV_8`0A_m%;!Ew zXI;vC+`C8GwjeFeOLKG5j&0KE$j&l$t9SY~4R+2jE<^MEq(Ks6JxjFH{k@SYZjqBl zbaP?ch2RGU2BY&>$W7Q_|*v`As;@ zv>{v)eh7P_?oA-`BIk7ZYn$k1>>AcBb+9nI?uv4C@jVycd+_|wN=&l)At+!&&;zy1 zOErsAMY`fJXi7+Bbe)D;`A{61>E&s9BZ85{de8B|1;ge-uUSPWbxyzjh>l&7u8q>VfEinJ(d~huk zSm*%D2QlC6XOo91K0}lJ$9GdN@I78Y0}eEO-pmz^wq99ikhfM&{C0hob=3S{N8ms4 z2v&vQ#X?GkNULyNuUQ&MP!?dEh&dG{jtz64F;CnaN0(^PS`bfw_G2y-)Rh&j=rjw*IH1@`KM|pb7Sjq5F`b0Z=xNe+De8JG!J}+^%e_Z=fYb6~NwS5+OfuCi3)6h|2tAo{l$GwXwD$PJ!yPi7+zn?+t#% zK6-3LD;x+%<4|t!V2lY2v}*!JNVc``o}lLJcPH=OmNw*ewZcA(&zSe@m_cewO@zGR z@joK7PBd1V(6Nn?mV6i-XZau0*r1LPjWZg?N=7dW-+?+z{CC4n; zuDOO$7$^lu+ct|KLGOP0C8yy3bp>d?}fMpa*&^)+J5jo2$uO{pT;Hi=AG*AkY} zcVS@5Y4iR)WG0g4Si>B{nz4PsBhS+du6~|&J^CWuee;c~=4KVd%72CSw!81sy~ozd z4dLBKmdg(h_uO|#bKJx}_R1&eXd~Hv z5W`XpZEx@km&6bx#ry~a{-EQi8p6Mf&!+Nt1x*A8;6_9GA^0&)si!6?nfC#;raq7J z!^CS8%t`7k^8)=f=E6G2$Wz^;-cY9A2Ad7kJf2A5?<}dM5oE$6Py}VnY=S&wZ4%y0 zSf}M}#UjZvNfX^xZrQ_w{t}*uz@1a$OI6WE$1UkN^OOou;+564&av!xudRvv8qC&7 zbG=%FlsFG$&2gi=_q1I)|AO=Ms@J?q&-=<3>d}wCLW8r;l(=y&hrs!HX>l8ex*6sS zb9;Wfw0&ohH9q`P|BxSkdW`KJ11!<80Yn`+1>*&8pyELlA7Q#r?wI3gC$^am$7@Kb z9P~I0C5;$E8xkj8aZ8rR>oQV|kvngUTa6Mggbu`km&i!s<&xB}JZ17HU_z5gUL(_B za%c4XS0s!vjcvHf&(J&?s(@d3xbw#cZ1QWF^~EA7$_DmRWU+o0)C8kfCwyy^gpU)5 zwzyp5x`14svKiW7#~5RK%rB;fDA!CN+7{@RGS~^q*q=PwKm-aM$}+d7Vlaq>HL;la zKQbLe(hMEgX9D-}$}<0om6*ukpM&@b%(rmOsG&(?Y&Oab;MYD)Xg1FaGUKb!#j5a@ zId1|H<1v48#kKsDujt7lrLN<9vCee4Z-eHndd ziyr&YzO37;d#cb)Q85M*_U{kJp+nFv*Uxb9kk~|snT0SjRt%EDdxUB!vpJ@ z;n;R?8qDK#CmT@a@3S<5v)i- zIe}m;%Vn1=`L!P653&@*%=uIPR>Z%9{f zK*TTw^rAN zT3FbjJUR6DIE;-rY-7@H7o-#VEF$FgxeZ1H{X~Hcen=3{HpY?IC*6DR0UbDeNdBvi z^ZU-w_A|~Qw^CirXr6jv$bN2hRd-!`o!;~IcWA&591VSUCqHmhjK37Mda0*D-GkN= z<1#=mu%V!eMkz}(+?w^#33$Wf`mBBXCH8Y0qcy$oMK9F<^Uwcp?K=B(t=@E__MLUM z^76WFyYW{2`A0sXPhWe3j=0T(ImS&-qTY4p$IHIvm3ryl`x^C6+osjtSTlPTHM?V; zjeTFU_JMudaAlqOv!M;viZL;0fPc(jH&NzC!vJg8pA~uDnirNw3hGKA(8@qc^iw@x z{4yRJRm#-UFeVw^vVD=R`U8pfk361;N2E#>u&m(+5@lgXs`@=uy-H)^Fl7C(ABh0D zwu-vzs#1cwJPgG`IfD5vW$ZV6v+QZim&YniqfHs(lFOvz@)dldxcQ{0;6X$vg7hhn zCS^}W^(CH^sONz|{pkTvr9{k5j+7h+V!+_?4_pC7B%{Kw8oT9EhuHg)$3H&IGS|G% zPxMJ%OllG-000mGNkl0EvH$nZ`2wM$(!MZQ1eRlBJuCDK8J>ky|MdRK|3n> zy;s3U)2LzWCVY$tMF6#r=PCvycVsJ~hg&8`OS^CD7NBd&xEU)kG!lzUn1W!Ei@`0+ z42E!kl7^Z$tW!}{VgeM9S1864yz{I_B?|Nk$O|CK7}iVV8I~vV3M@N;cfM^DUGf7P zl?Oi5d;#?)gR&)UYTZpZ6j^~hQw~OoMwxaRf`;W28qh>K5f96wP{*uMmsim_6*?6* z(`4{eOfLU|)uZL*U`~T;c2KmMS}09o=6qaL0!u)mNNOtNNzLzndxUQux=-6C;6a!66s$nQEt6;OWc1V zmO=cyVQOdT_u|<0F$t8WfzKV=dn#Dt2p|JF&CV3r=2t|Y>L*=P?!S&dj7<~?Z z?ZK(19viQo=SHumK75~FvWw1ynxD>h+IptbsEEs{kk~9#ycnx0$w(=b5cy>aP_7EpA?ax=7UW% zn?*)fVojSJh=a$1X4r`K`+ZgP?J^r=|06DqH|{`DONz-Ka|ZFnMtLXjt`zUr3~k>B zdf3mzs$e^hT-HI92?doirrBeKRMC zvOyk5xrZhxPS!Epwx{w6vs^l@)D}K62lET8BTj zyrkV6+OK@>b9Bb#m&%*QOdqYX(e%GA*_&BV--joHw!zKO@Ys^7wRO!jQsSU|==0a= zga7jl`h_3d6w?O1dsR7VHBeS=VFg%4$W@7-)`&?p$Htd~ zS)F>^tN_-aJ~sOJ(_>Eb@TIn29IR_(-p_2Tt!hNf{I2GA5RJJ=xc!b>HJI(|wO{vI zeZ${>y=E3>G&*ubv)I=1hQ9Eb>-5Qwe@55caI5Y*a!iNTHY9FF6YJQW+^oLv>aWxz z9&@p@II9hQRrNr@9Pzjc#!fh7_^HA;0j7E9;X6{k z9kB^QY7A^N!I}-tCqSgtJy|7X&4j*V;a(N?y+;}G0`tpe>f=S z5>a$BO&Mndn6Syf?F!Vuv;fa4^a=_u6T8C{EVlgY}?QAQ5KCo`N3*b_c^k~YmjqAVJwVDhmz(+(#)o0hr0mQtFJ^G^n6 z87IqoXtJOkLHnjlkw$7sgn5bKq3-dbS|Rs~Wl-1ZSBV8Qc!3E$ymH)#ZKh8(d62qk zlT=4q+g<9f8@2ZZG%ko&Vk0T28yX!iau@_5!HFkB0prLq_4qeDCWDxnFJl}d<^#RW z&an|Jw4dV=t>~}mcTC4>ikCt``hg(EVTAPp^&pu*z$ZXdGtmPLar8WEX)8li=Ci5F zLpcHZOVEf=P`}C(e7A@QMF8yfITZAIef8P=^x*q__+DQrx|5q)ufR63@L)a>QPd(@>V&z*bH)rCx{5>L4E!AJe%h0t z&f#IGPk#1V9XPV2Ozabf#nQFRtqKDj2m|p6`joaRl=#zq3;V+jZa3IW&kbtr+__W! z7cA%I=j9uSgUc&gX2a3cJ>|MD1+$iMK{({~oe_vd-hs3=fPfEh86ObKtAAI+B+7#k~A?g~P04V`+%b{udVu&>d1{1jeA@fRdpZ zDq7J(?L!;Aezl2*|D7|D01srLsw&OQ%xG?Iu3*4{+;ga|Y7Qf*`y5&~gbMvhPU{$lUslPE)a4`J%pT14M_shScpZlTzpf~)t z|E@c~c!Rdh%yPb&(~!eYCaAdy`ZCd7Rf{GmaUhIwxC6x37J|TN3r%?kW(BGYjh4da z+{pjUsk7D7UT2GZDTG;1IAh3`>OF!{!etg&kKC%GoNSrmAvO1b1S&8eO}M` zis$OZU;Sduo^hI1j~!LTc-FCX-S&xV^x2PmR$sXGX5D)Cy;>m_z3n?Rq@Ie{KjIOO z&`ZDO<+|XKM~Dr8JQimLJtfw#AqVk$I9T3#&wV=NzfzqB$mf;n8D$@&bD|eQM3&e! z&}NeczL%I1%Mh^x1YRf!7mkN`;&`;ltg`3KSzfBlcW~P8)kHnLN|!adi? zfxB5-lhI@T_a*hC5-lrkh!R2C&!5G;-KdZ|x zdyHQ3)vwUked9OirLTUK9`Vd)YMz^ZuH>bDPYoC|=NsOUxvmveQWe3FtJE}Eb*|MP z52RyBr2@@JM$aS<_vs>pIN1Ur#ufLUkhzIMmNGcNvYUNYey20|k}-by^Sb zxSvjyO%)j-L_+`N~a6WJ*nSu(`=+k8NJvia}*y%GUJ&8RK zomV4EatwfNCH!<$9e?>$K zBwfA5K1xZUM7|N?L00l$OB&jJoYnDPE^TcGwa3I%N8N^&FYP7vrTu6nP^YAtiP!+U0tPA&7U8`Iz64GXcEOk1o?+YMoM3Lz&V1?1s@f>M&&!yvVW zHlK(fo3~~e*A+T-ITWspB>y8bhxz6F@cLM*3ax=*sx+caV-AR8=qN}dZz3rh@YJ9T zEtN$1{!97Fyf-@7pm|fn2f>Oy@Lw7A4WY3?ADgbOuj$C@st$5%c;~_Ubn~5e>fT#9 z^c_5?;IO2@j5d~5G?#kXzipRJ+rCSQo43#Z#YgljKl^j~{(tg+>oBP*-PjoE^TP(+JN z-Uegv(_gq=AOFG)s&?(w8IOIkw77>3?J3o>l5f_y$-M2l>-CvWeoD8~zBS$#BZWqc zIeLbDqcnrfxs)`|_+F?@13olchh`WbHRHYFqGG03F;^N5SC=&2SkdDAKremSSLwB{ zdzBV;@6h2JZ_q63P|*Kh{N(5Kk@tT@H-6z39XYbX?QyNawjJs(F6zkIini_Eq1SxF z>vYy-?d@RRSk)j^8q6+8+y>ot=iU0q=dRJM+$tXAFffe1qM}W?r#y*W$WP|0MGzAK z(*nkY--i^~)N$NeZEFIIl<`kF=OHP&E*JD%RoI@I93%6QzvUY+)!|6 z_8zm3t=e%#)?;rvgQZ#GI!n78ml@XSKC$uGz*OrNeUZ^?fq_X{P{VRfo?K*;L?PCP z3FyNC_JX+?5sG*iP+KNJ9}Xhg@(@+nsOT@BBL-RJ&^0rXW*7^;Av7|Fv$dngG-963 zb0fH)8~rCd^+~$=WiQq%U-N2R_^fA2r=RYC(2ew7e?}XOm2uR{%*O;m7^iPCx4Is4 z)o@KUbw}Ez-GY#T+!0%V=S(|_ju%b6!lfn^}ucYv|tL-CVXW3GIh zHc&0;zq&7KDWe@)JejU3tN2V-;7AA0tBQ;s3uWaz;_R3Q?FnL0p^!iXnbYR}2J&v9 zyW9(^c_|bu(xxqRl83lX;wi@ro^X|4yy`0PqLLhI5lfxqad&0prjWl8pNQCON5mGN z67hHdY!;dXBe#)5Y#Swt@a=3Ad7651S5LO?R>pLlTT&?^3RIrp8?d(UEhCX%l{rzV zVhmKgz#9=ChO0_RRZ4O_RX`^QTgjN6LYm~ANGt5QY-yPXpuso_ihJB- z#!hBzc?~peW$pUb@>GJ(q!=~FaX7I)=bk95Qc^@Hoq&+^Kydv=rhYxhCn6NFg|3)w z;DsvQhvF7**WA3m>IGNp$l+DJ|3jbBa?{hWHxK$+BOW8(B~_KmCL+;y-Vk`rN+I!K zxeh262@m1a95*pDgMljgZh-^B;^H>=npbPqv6WRF*;wOH)o5HNH3o)wwafj26{xT$ zCyQ;BR7f3lJ$m?$H^U8^TP53Qu-Y*8#HP0euS`N6Qq*dqrx6?T)m-b?Q1YShUJf0H zH%2<(+1i|*lFig#w^J%>*N0Ga56&?A-~IgARK!? zfsX_FV|I2{eQpd7b2EJV(lH%aS=RL&20!>0f1#Uiyh-AAPX`Zct_f8uYr2)g$s7Oc zf7ieN-tX5>{M#SW2mkEd8gjc^V{g~)ojUuRv$W^5J!%+}hmRc93J1+hUPDC341}@7 z4urNtEo0~)CBf%J|EJUDQBg$D54q5YHgM~-=P3K6UFTnSohyy+vK(tG~wJ^Bo{>-Qd7 z)`5ddnpxOEycc!g&_ND`b9&OVuhiwwc#^c>-Qq|w0*$1jYdU<(UHE&wKJ@u(b%1^1 zVO9Ltz|A1X*JfN1#EMur2BDw`k%1B$BI>z#T-q|=530v~ysyfP{SJZ;HP|x7%z>r_ zvIMHgc#U*sfAk6aOJ?}Niu z=sQ8Dk+t<4&&O8AH0McV#fO7wI}ek$R}UNboG6@#L63E!Pecc_$G3m89OULm?;6Z$ z@8Wiywr#t%F<%!J=5;Q&f6sg23-tBh@_*@tuY9R4efm?RGxkdKkJdLNdP&ucQaz_C z%_ux(I9U1zJde}Fnw$6mqGl{t<27k@S-SU-?)~EJy5ZW}^qFgJ(dX~HPd9P!yeBav zdr9kH$U$bTu^P_<#>x<$CX{Na^hf-k_IWWTVJfc>dyPfe>$dvs{wNq0WG4Q-n`ds*UT62(rYj!b$i+e=e2=LdBN z-!luLA*g7(d=S(~rba^6PT+>2k=0FP%Zwo35Y!89CT0Q|+l9h+)ElB8o}G?)=XJ=$ zQ#_e689qZvDo^;9W{V^StkXCuKjw=)?6lmG^ z^WEh)fe)KJwNd!X_%OhxN*B|UG+i!+f+AX-;;ZW;E(uwcl4_5Br^Q2?c$>=YvK^N) z$oU)ni3r-LQ^ZNCNw5xDU&AoF_AeP5%^|6CZC9q z|2XMQs?qh88JA}dp=GwGIKPO z&V{lfLEAN^Rq%Ya71I-8d6klC{I8Pz72ZnLtK1$^DIurL5g|Vjp@=ZM3D@m}X$Pkt z3?ib~i)iU|nX%tJ2x{85dvQUJyW}xC|Lk-1+0S05+wMHb-g;K!-kjEnt^eDTs6sss zZUfpg;GpjJc2K^2a`|RznhE(&!#k?9Ju1!3&dIk%ywmt4PK_BI^=(9 zy}@9lkYfT7kpc8|z-^*Z1Ry7ZS{^neOaS_{y!^9VXVAaNQ1C~ktFtPp>MMhB>T8`t z;S!sydpQ){%Pr^O^>uCzhguirc}@%GcwfC>3|1c*2iO^Ghp86(x(1z z1sm4Le!njt0_(b#ZwC(@I;7k0IiOpQ9#OM>QBQdGvvl>#U#@39>)AT@(#xcoIUT(9 zc75bs@78bq%CG9D{{4T@Z~xk_>AFvUrtGD9wByL(!>R{89a%o6n{U5WciwwIL&i~m zZboAQd59b5^8fZjcNAiWl7WbaWci5qVFT=^Vc!k&#x&ohEsMYy0>ZByXPm{R_|@9; z*e8lkVJlTjL-G$Em5wgy!|!>oKK-$e>y88WsOj}p^P_~SnoaUp1L~eZ)pS7r8#9qL z%f@$>aX4FrdNEcl0Ig!gwq%2GTh)*X(q>)xtgG}@uYS22SMH5rO=CRL4jt8jPkvq>fB%Q| zx$C~D+YcYrv8tyP#@vXT!G?`}G)d7^QdDa2J>xGUR2_mgdBB<)ty~Ipb-yQL!)Txl zC0j_1VuFPIikvQOn%EErF{z-lgIY7J2Nm;p$PWwaM~`Z}yrdy?`M&$qXFk+JHqjhZ z8h&KR?CDbu+926aQB&T81S|1nnW>T0J<&0Ew8_F){_qeHiuf|1FQaTo3GW$Zs%hka zKAiz`+{n)}M(64KReoGqWgI8!Is4qR_4H>yLoa&8OZEJhT&<@*_nF#%@kQ$G-6hcn zq8;KOu9i)`NaxOVj0yElJBI{8EQ#G6N-ZB?)C9 zCDH{xc!A1k+EC_DBUQ-!D^Cw6dmLtZI9%W~1G(jMS-||ltG&o$w*tvvq4Xx4Vlo)t z)UW%4I#~}`M97=sQ={T*!Vmk@p;o9zH}oQ@hG%_g+i3r@CiXM5h1=h?-E=zL@xu24 zY*@#9rk$RkMrlAf7>=oN3!c1*##txO-{g^rm?@7X0aSdL7A%h=Nqgr(&mKQ*BJkrum2vo)>t!n>Rfb zJkPp3@kB(aCt|Dah?b8C`H9$~5m9_p8Hm~0_d`Uh6A>jZBGk1ZVoTPwBO;FTAtFj0 z5usB(nQ?END*t5ih}K?2OReJrp<|M{p*ZiXGxf};KV4tEj@ya9_?S|EPNOu?s2*s{ zul^02j$nh3C|D^pV7@t)E^pXs>r;@667R>2^`Xi%B$?<}h(^47hujz(<1pooMf?BR zYB(f}c`H#Uq>8mMMdG~~*b2f8;6zX73Cs(fh)~2sho~ddk6xqwuD}HDWqCazi@GzL zrVw6Gzu%l}jX4bZP&kTO8|1F=9$KO;N7(Ql<`?zD+&mqHE^lmTb;NJz7-}$_4O3rf>bef39x?&;IIHYWu!@ zy8Yvy&>#NdFX=!1)KBTxe&HAN&Nu(D?z{7L&GmYk=QgO%Sn#dAXwO}T?$rwWj{f!q zRSUCP;b3rto90oH=67!AKqUGVD2x3m<0&I+3f%Wu5Bx-gB0^moO(UWd8Y^_!lb)ip z9{U7oj=DH7RrRdI?cmXyZ_^h!2!5K4?tL6gqCa39_qZi&lXEnyZp#DyjTu zMjUc#{4W;^k>&sW*=!xOxzeB(<6)#e)PGfU_f8IYd)SEcV?du9u<>no>za4HUmt(> z`*rBcy{knFbf0rR91gHj1Jd@{R0$U%XYH zdhcK8qZ|ltx$Q2kb0ApZN0(JF%2e$3L3^Zwb#Y8g8BG$iW(yf}8QBzQi#BzUlg<+~ zY*wkmU*5ra@J{kbHOD~poZj<;yM(7zC>c63| zK~Oe`_c--5&WY{}CDJlB7;{ICX?)x5TKmFv`pmoEu0Q{iKh%5w^v(K<4}L&jxaAhz zcI1feVeML~E3Gm={9{gVR;pRI%h^eo#H{ByRgpTyCsexEI_|4Z>r}k+6J7yxD^7R%lMc2U?&0t5R>4X@!raZ4PiFl>?=1aj*>g zWH?Qk5(}Iv@gN=M4fxGG(xROq9&C%e;vdOW_)L7H!jrB*pzk!{li@N(P^P1nJ8gI& zc9yZlm|wZv7|ztXl*xE z5;-pE*kr!OjDZp3!=8~<3^>2=<;yt+36qBdihg+?do+#K8X;;K+80x+K}^z?6x;{eY$i) z15*>A7np?bDK$YK+y@Jq#)B*MSFN?#n9YhJ?B!Jenh5E{%Q}4UnC?DsP$M?I-o%UvhRe8pLGUt*wr>lKGE1pS;>+WR=At4$NG$!5u71*cDXnhkkALU)^rQd!2lX%i`Sp#WEnmD*nKlh*uPg6QL(ud6ndTxbKj9+yLSfZCTen zx7?<8z4h(7^RBxU{hk(f@6?zx)$;n1B8@aZm)LNRH6I&V$m<1j&6;MK6>hawG#IaG z(5!2*7uv?Ad!`D7|5`Qgt zR(AOlp;F6M3^U;ccqM>mq#JZBvu>8<1}##i;4~~ZTrQP(=5h|GSu^_V`3H^EgZ8o5 z!%~kR_4x5E^^z{W;xW4Fh0oU2U;7F@?dlh3?;|f%W;~9ELuG!5644xU$R5EnLKG<} z0weU5$7^C;H^f*J>yi!~*6Iy6>c)?JOrLo7d-TEgzDMtV-@Enxk9|nj-gcu7tRK^n zyrCnMJA(e$@nKzJg3^RBwrMM4Fz`{(%$OAIb=y#I#oFX)t6d>mi5%0wUIKwuU@9!1 zLW-a`1;-4sF@r3-A*QzCG*#kvv`IWe&t>0)u>n?U=v~Psl0- zw}aG&ZYmfnQIEz$1@ndzb+#;{5Fs5cmZ!XIcO1!aP3d@uIFpcJsr<_+_f3HwGi72N?nkss5ZhFIIL zOgpW=g@%@>K)9nu>X7lRK|iy=X4)bY!k`XWPCK)~xJbyV6xd;5WMGeh^CBd_RK_fp z*{~gem}wH~p-@w*Lq+`+@r|+Q15uc`VY^ZLCr6g;F+|kC=?-OHBq`H5hAI*Tm7+|< zB>xFa2lYTSrY#Na>Og69qM*lSV7BV1PpSs~>yA}jE2m0g)$j4kT0kRJRF#ydqYBCr ziwYzVf&9yW6w-r4P>+%Dr-PK#BA_Avk-uqwfWnhb@WB{J6POQVW8yy|Mq|d{n7$kH zYja~TN1BF3&-1$s?lHp(h)zT=Qx1XZDhMgjWvtZ1rdL&Xlw{jPhH+4l22tTfAVaihn1y15 zob-E4Tskzw&Dhk9>{+8RacF3pBJrtYyA|WE<|cq{p?%arzsWj))~iD%y@?P{`I^vB z6-gE2rN*8qRni9mBzGmb*iWo+(unnI#M)#K4&_8u8xc zuk|GkO3RJzy!)u`IJBl_uuVC0uo{W8XI0v>o`zaW8ycpu##E58849HcRVj)6lIYUn zQ-ypQiL_z;0?N2t1gR+`VTUNxMz8`$MafY6WZK!NafecWqQ5L}JYR>zcr5y~qM@}K z+LWsq4Oz>5n{z~2_j2QUH;2MomyYQ!4oe3a&`WgUD)I9F$IxX|4i`w6eA$ywnEo;SuX(H8(?Vj&#>Kqd%wb|9}34{^0lC zr0Y4{40^LV?X>;!t=2eKTII&=*z!7cjWlNbRQMc;eqwx^)`J=KW@Z(2PYvx;RpVo= z3_qgXBgV$Kj0^kRB8aI{C@1U?_pzZ=>}ORng1Asz4fPl-Q-UohR$ zAbs(A-T40Z=^cOghr0d?U({MkI>LToX}HS8;j*e~4ckXGm|fRs{ci1>ZFKgmbnZgZ zdE0AUxNAn|gR{5ybjD&|J1{$o?{iUUwi;;L+zxG9*rRK&yIDW>V?V1mzTuDc>;LVy z_2G|wLb+cvmXp@#rz$Z()>kyc9(4V{eR|H7PuKVSgMX;KkG@1c0M`D8l~7R*jc&YE zfAN+-(_7#4R(;_Mw`*p8kGAhRL&0t5m;hvKUo z(1?&|_o)!H`&3AY&J_Wp?NX(fAR$lGnUZB%QX~^^8}1h4w%0 zQPSzVByQqkeqL2IgI|5T>Z?jKs-q8T)i~0lu0CU?POQ?S6|vw-hwjn(_1Ea8_q|si z`r|k2y>Ivfz2gu5Sa0PAjgQ`To47C3eRGX&-ndUklE+%Gu7_$GQa7Q9kbk5bVsprQ zW7KRg4_UD3$DG-xK|^Eo#!5jtwoWCXJ>XhULf1gl&Jp=V^Mf!XrO zEJ~$5+GoDlEII90P-6jsFBEZ1p(1|^Vql9HwFvZFr~MYb80HPCqa-!sPpd{WCOlH| zGGfb`LM^j@rU>r$$(vVb6mQ%{IgPtP&^BS2CXd_MsuuyHNk*AKaBWoc zbcw>dNCADEkQl{2d>GUxQ^UMrUfq*njJ5_6?I&8&eU~I^rqi%%PIr3$`>CCJaA3%* z#51<^;Zz=vZ5XEwSs9p5ylfLMMk68=5ooH=h?r0!kA4(%GN`~rCY}KPm4s>f~FpaDI$b|%y$qE=J^&` z=ur+d zd8V&12an8KJZM1cg6p9LCX>FvWc#69%c$)Sbk?~wlCrjMo71kHJIep!U@$YsTf5d8 z*H|MCQ{(8NQ7J`*r&wT&`XJV@b7-iyAzkVwtNpm5jIMzsPXO_UYE8&j0mgOmCl~@P zl*4XL)EGI`Nu~@^YBRGU&?6-US;Rqpn8Ej(G51Dz8O5m#_tHf<{x4s zeIGYw{@-jIT3*#r|2H~o8(Ll;5{E|RuXwRB&F&cvhYdXYMFpGshC0{z(RG78Lz6;H zNbA@Y{&{-o*M6f+Q0v)>k3wqm|{z$+2>%XBN`Qabe@BHTP>Qf*4ln&l|NTZFh#_R$35Ryt`;?hjo zn4zMLnYL$%d;qTp4W000Wy&_#we4)4`f|qZ1VUDTT*f4wgCM}a>s{~BHQaU_z2;io z^0ANWqi^{Wz3Wf^RA2n!4H}^{s(Os!9*3z=RRtU7vC^0uKz?jEjRW8IiaAh^v+jX3D~$3*|N?}iR57-!@ot*xwTb!nMH?*_N>>+-+; zT1)B^lRmUZkoi0*3;+NS07*naRB0-Q+*&hHsZV(>o6g5xcBx+Xnpf*zegF6AXMgIa^aKCmU+P=F`CE16^PaDBF1|#w=!p0%9XZNn z)DazJjIMIi)DTM%f2tDQE&s@28P&qa@et_=bQ~oPntn3x3I{R}QpT+Hp){|^MEcYa zPg^$Z)Rb<(hqfN3oxFD2gTjkSK=~%78GTLQawo!BnKh+tGvACa5tDvZshRw5c?Pos zU3}@qdeJLis+YX_m3s1Xp033U&emZ6ZjD)|M`b+}V~Cz)Tqr?V(?t-Q^$mXDSyJPh zXkxF!MJ*47r3-hV!~L8EEx_#rzhUq=7}x2 z9E!x@$bt?kl{M-VTlooTdc9O4DZ-pKA15O^8Fft9h&Vwv%eJYAvMulQ$zWePJ@eKv zH8Krq^Dwjd$rG-nRL!uX{x&Jznrdi!rX3xeo<=9Pq5JcW2gXYIMs2>SQ6`IvZ6e_S zA4T8;aN1=ks7yx{9&OX(K%Pn+)OE7_N!v5j5QD_j=y=l(o!sd>v3n{?b>$P$`s_qR zm^^4ioJzlwo1PlMgpu0(nBv7#BGP6_6Ka{E$>X*;E1^&(!o;6sA+d5CCaP1!7K~10 z{zN#>GSk1kh$w+90f;y`Uo!ueenp%*CMhv0$&Uy{w3rT)^G}9|C~6U*EDyrexU! zM69gi_NPj`Fp-;&K>GyPUYX*B+4}N2;8aAYvt31E&oqJUf`~1f;&Hivwx3YQ-c*uW zWYQUFM96R5UXM*ZH+>Z^@WeY-Y*zcci7WWp;QjNor#wZ^xaw*8_$NN54}apLT0>`@ zm;Q+T#+Xf9HWKClpHfqzjwC;R3sM(pr!wyVuV+!--(v-J^=Eq8cgB95an4!Vb;jwM z-?d8{{K&A%VX1K;5VXCGlWj>U!e+>kBH-0N5nH?^5m9s_qUc3D6e_ve`yr4|U?-e! z+98}*oXw?fO&)Ke9DBbmjWL2Do8vWZiI&#Zb(Br|;iVNFfgUA&j00i$?|s;Gjo2ys z@X~N_Z+d+WF||e<=9f7X45L;xGq2R2>BlE7IpgB=V_JvJMTTH+Ya2L1BZ{O zsrtI&@lPtZz5n35|B1ftd;W z2ODl7MjXf*HqJqOYdUfURBWE-=$o^-!8&`-4xPSpLHoANYA+k|J^jA+ut`5HYVBpD zpHBJxl;5k+PR8r}#+v#oOVY7}+BkL}cO`3z1^Q>(K%*WW_IsLJ+^#0|xcxq=OP+p} zu6)@`b^0@&Chc0(n7JtCme!V~JMYl7{?|y~`{%mtn(LKU*7(uJlWr{0_8B()F^N6y zsv!;yO%LXd@Mlcl_wlcXe@4IV$^UDYg}DW7TU^xQ!h&`{=V(*Ek^1NlK%XBW<~aN< zj{s%BBkxyrI}f&swI3HatOVH9~|EI=O5C$-v55xd-RBwHaM)LB=yj# zYvmMeTXg&rPU44&dBeQn_G4bgL`B`xjWMUqn;O%+)2BkjR!tFGy!9hO5n;B_h$)_S zjER|%l~`^=6^>TpP;UaC-B|+VXGCB6u(d`QmN|U+0Jg?W+u^0d3J!dae&Xf2>Z@L$ zXTJC=bmo&DtF-?#X@S@ddh$KE*UQyrq#^6ExQ*5fhPaudVx_9$w}wh-gXr9QRO2sl z=l7{E=;LpBmp=IBcj(=30q=gVKKRK`>QlGhqU)9q>DJ-0R+*<`#=)3@HOA+R|3(89 zZE7&r=dYQ7H$19A)Eh|NN|J9nt14eKOv%4y~TcKQM^Y<|iT)5u51; z!$h?D5iOlA6Jgl{BO+0x)j5%OI+J@c$VHsz7(=^HM3ixwphg#uFG<~DHd5$xjTUzJ z+eahf0UMo({{!Ix3zHcUiukJoRs5BHB4X2i#PNJY+^=m`jtG;2#EU#4PN_GYZ{CPF zxqjCV#V}vf>5ez`uiickP6PGhKGr)KT;^nQ%WS~ZnC82*WgQ}dHM>Q{8W0hxQrbi! zZB*nL5rKYcl9BQ_f)2Yj4DmpHgCJoh$#6Hp@<1Ip-!`DRZc7)iPaZsZOVFu zAX^oihasETX0*YfU`4@(v}O~L*H=`t)^nYpuYcX&(|z~dt9QKn&)B@JX;kxO98~h= zrGalMDXvESkeAkxsRQcpyv`;!V!zVnbeouCoX>Y6u5M^I`4>qSg-pb=Yn5y$h#*_-5v zCnET6KjtDr5zz|#U3jO1eh79a^hYF(1)w8^HrQlsK-UP(Dx0uX=<=wMvBXAwiG$-& zZlR8VqioWb#wf7aKElEA-eW7=c-5L`qu!t2uA{3Py5-IT^w(I2IAErkc|GI}w9c*Hhu-}@{lYK*s($uA{<41W_y16z`TTWS#%AL1nK(rB zI22YKKE}NBeVen!foaV9KIofUyJ>o7Kq38nHlYzozdyr7t!0M^LK66 zg}b-u{O$8Pd%o5_cKSQ=h8ELEGhlJ9r=2?%RnCI?c-Ita*3eeXTrQ9t2I>UjmX3SLlI5EEO zpn4qsw=sVAae#dEdFSZ`&v=Hu?p3eSw|vt#aTECc`c7_`pL6wB>4HlhqlJC@r5(GZ z*?Eb2wX&`|zHo!y|CT@1um8%g>W6>)C-fsf@ss+`zwsOT(|5f~M;S}&67i^&ISe+b z(iq<}YnvkYgpyj2QUu>q(9}co%Lt*{TS}f~kmEkxxH(REibOKle;MI1Ozk6ooBa6e&Ya`^{S8ylmrBNOHqKppaNbV*>L==0q%BV4CPS zUlW}|sWccKMhYnr`M~+0TFXS95fO?o1O-J7E#*}n$2Z^%XPX-49U1f1aYM}8uH{af z@2L5LqLkJ7kw_cPOVsNg$fXAy!IPtuB{n)4aIB$xhs|G1vW|D&$#}~mLJ{|e z^BxK!j<@kp%Xaz^(XnFgi1~CJEF(%XHWep~OGX6okJ2WU$C`~w$FO-)2O%qPeg{b& zgvLp0(BHZzqK#$5@$_$eARa;kBVtoKVvFyD5nHkcnv2u=5g{J{JLn`&1S=TpO5=3| z*!DDBq)T@?rhf5t=+dT!^`~jaTi(GkW7>X~*HPzvDRgbCA}J<+61-tqm6ECikW~o^ zBG8;*+PBHaA>0Y>>l7biAPfYfH`HaP9CR&8E6C^ct7tCu1GZr=<#~vgLv^GL&_T^1<3Q<^a07wxk{#klDorX>N{#W~GB1 z27TZeZ?-2C(L&`3zN0L4UgqFvP5>1>O+83Us-sSrH_RK6{Vh$)#8YBRpP5#~uFy%^ zyvrG=Dk7ACcsiMNjG$};)}f*=a?pDC#z?gq4qn+ee^pOI((4>gRtVD}HgN}6*L8rK zl{=3f)g9cZ+e^$5Mb5NiA;!V2q;1MM@<&U`RGQIq3 zU!#BauYN$U|K9J@6<=|+R1b9fhd-`A`k%kAANz@)(J%e#f6-gG9lZJWJ5>#4wYYPS zdNT`U4|&%;_v+q5hjf(s*V$MGoA!!Bk*Z1?-6^H^D3|2vEw2*U$@Jpa16W_o{R zEIE*j$*!Y=y4=Y`vZ#BWSw*rWQ0tK3lVIW>cMZ3JUj z>?Frh;t<|vPc%=pJ89=G4t_P8?Fw0+_V(+Zs(xQJH-$as!OX&}c5a4&I|Ua!7sucbpu=Hke%^N;9yZZm_;@L=1bW_NC9u7%dw><50v@|xD}yjMGhq4|}O z<~AD5GF~e0-07zt<7x(f8-6$t`?FqMYi2Ouh7fR&o##M1KR2U(3J$DEGa(J=%lRDI z#W*};eo^Oa-=Qa7bdj!p`qT6cU-Q-ahu`we`p4h>UHS*#{hfN*|MhxZ`PDDep7YOE zwQrAvJ#pAPeEH4@umAuM07*naRKsn$_HFOcAOGTi(a-(Bzt_M0fgjY5{OC{USN`*_ z=@0(+E&A{$Kc!o^t*#apbl$}mYn^#8!ncMx$5pLyO7a292*!2L7hVT(Z_>jmR3|*B zO-6=ChuZPzLKD4+twh8YKgs@*Bx0*f#1`*@5upe`EF+)*fe58qzq(PYl4fAK7|5t) z`C^NoBvdD<0(BMY&>K|RLwq0km`CYRmtUgZ?j0Ie(x_Ldw>YoH1&CQitY(Pc9DP1R z{lPd9x4UAT=yrY>`qbz21#X+){l|Y&{yO-dzWFVB+uPozYq_nx<<7fwaA`@$7&l7= zWBJFZGB;S@0Np)Okr3P81agBoZfK0mzvhpT{p@mzYn6Mq4B&Uei00V0iX{H8J z7;WWpteB~j!=5-`gzN^$6KIl;T-JjIdJZzp5V4gHSHzHhpaP6XX@*4HM0L=ZP{E5z zg&1A*iX&`PM12`(!aH4f(yhI$lBSH)g=aFmObhbK!wJ4yp72o-pHQij@g$cGY!>^} z7x*jeOv968I^FTp@|JJZ*%3Bc`VTfS1F(y6E zPKA?~+>%CwBL3>IRnuP$7t#9OS&xX0E$!+O)A6yaOE;_&9XgrwPlkvs+b8CqXk$8U z-OhF=H|-9DBQ`zeO*=T>=*pR%n6}KAwm;2h)=1|YmQN!hN_-+hp08C(swb$BnJ;oS zW|cu+LQbiopGZf9x~JnjrOj`G>0P-(9tPs!Taib+}agNbn zM>)73g&yN($Nw7(-vHiy@UU*a<8B>VTGg57U8tA8>TC6n|LH&1cmLo2ufFmdzEO*p zUZ&N%?$$fr_(uKQPydX5@jv~F-ul+J>&BaI(K5F|5ubU9tx)UuA6r$$U>Ii@jruN6L| zh)_t1hywQ?DM=wsuT$)ibZsx&CXQ+vX+OqeJ)Uj+<`Mr{4QM{o!x?rhfise_lWJ zQ$M3$`1xPd@BH@f>g{iPr{4Si59;&RUav#PmJ~jm&o5|$cHGA;>D~OGF=AYed%gCr zKr>fHqQ~(&7-IoVw5dR$LPNxSz_QH;^v|A#GqMQ)1DpMb + + \ 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

    5Ydb#- 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 eCki5)LTZuHIMj-?=`!Oln!J6jQx*)}SBjl-xg* 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≉Vh|Y|x%B+f*agC^O5%;-X>q#h9 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 D>kq~A literal 0 HcmV?d00001 diff --git a/images/news.png b/images/news.png new file mode 100644 index 0000000000000000000000000000000000000000..c3a162c6153b6a933252cf6369f0f10d404e1f38 GIT binary patch literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^>_E)P!3HG%MVKuHaSA*liy0VLJ3*K+H)HNEpde#$ zkh>GZx^prwfgF}}M_)$E)e-c@N{8UdD$B+uf(!NIC1_d4#ZQ%!OR*j|1ZUxCD zOzszWOPYWA-dZA+S=3nnk8zQKQqW?*G_it2>jc{ak6z}u8J?92xgY(t`0u*9MgMpH zJ-bry0bi+MVO#T?vvNHN#>K3gKmOuy|H}}t(?R&L|CUKW3mH6J{an^LB{Ts5CaOiv literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5d68a36ce427ab3189d7d421946b8dba15c9587b GIT binary patch literal 461 zcmV;;0W$uHP)8j1Z9Ja02@#yfDOn7WCP3s2~R;XiqC0sTy@uzDn0A# zMaG<1tQgL@63Qp>mV{%ava`6D5Flr*{mfPD`~DoU7wB*NeK>|g&ajdO*;zp~?DKeB zt8UH~;NDkBp{6K`b=P%PJ;=!#;FcO8e)!PLD^YcOh_OsxyD}Zpl`6VYeB-^k&kzF(74n`Neg}#SeJU) z7%e0|$HhGBEB!Wh$X(7}3c*n14av69duRe|5% \ 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 0000000000000000000000000000000000000000..d6ab75d5e009fd14f29a6ff83fdad2e91c777583 GIT binary patch literal 442 zcmV;r0Y(0aP)1kZU9WsZqPD8vq8cH=ARM?6UDZ!B?clt=@iE~ z*+2H3V+TmggbWvAjH!6JrVH3f(qKsxH*UqV!{q{+R20wpB%AZXPbe5 z9qoa(qP+~Btr3HMw7ea&?%8_YL>V;+_JOb*%F;2caB%u};+_3xX?%28j__I+96Yw7TlscNmZiV>d$wD4`V zjBIaTzUL%t@0g|1lO?l}l7wkV!n7n|TK=W57SrZu^#A|> literal 0 HcmV?d00001 diff --git a/images/subject.png b/images/subject.png new file mode 100644 index 0000000000000000000000000000000000000000..bf3925529a8aded977e6e899b6a8ee2847268aa0 GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4aTa()7Beugc7iZtZpPeSKtaah zAa^H*b?0PW0y!+{j=qiz3>*8o|0J>k`Hh|~jv*C{TPIxPb#M@Hna(w30juf@7tRm| zPOAVWSq(?d6%SS92~FCwdhfn_&O7hjdlCAes@+p5YOR1|Zei3; z9m>AQ_sYZ0&ChdBlLMjpATV3Sn33TTGX?Pm2$H1}+evY$S&B%_YIvsbeUHG#@KON> zOa&#QhLTkV)|^Y^=ZpT?_RJNH23l#18Zt05Nz5#=TVF^23sg?z6bkXRdKeQ!r5iQa z2UT5_BQay0NjnN&UHd_S3KwPMeElG@k3_0i1I}xW79aBWfdIKQ0!Ul?-ki~r)r=|! z7ZNXvhTOFI?zjq-BP;8bW6P6$$#*7{ z`t0~-{lUddXT0_H)MP6^v?(H??2x-#jSG)7A>7s>VvmBnEemKDz@^3b) z-$uIxd{oZs-Yi&hSZa=VQ>cSvO$)I)=giJR#GHe;Q50j1GQ0hy{EvfutzMhbC}Qg< jnKDMLvH1LQu~*(-n4jmx??oRr00000NkvXXu0mjf-k3eQ literal 0 HcmV?d00001 diff --git a/images/view.png b/images/view.png new file mode 100644 index 0000000000000000000000000000000000000000..449f08bd6f3242edbe90b681abe7f4c05a8b7ecc GIT binary patch literal 189 zcmeAS@N?(olHy`uVBq!ia0vp^oFL4>1|%O$WD@{!3Opi<85meQL6|W&W9~1YAY*Zm zyA#8@b22Z19F}xPUq=Rpjs4tz5?O)#AWs*^kP61${y<&^1rC>sEN%u}7uO^(E8jT4 zWfm}5A+OF*=1!TkOh2!TlHY|Rim!y{S0}Fj?9jA|^_E!1#{+v0vs*T-sZd<9Xu@rF esqaU5y7*M{6_TggZ&(8~ox#)9&t;ucLK6T|k2w1P literal 0 HcmV?d00001 diff --git a/images/vip-card1.png b/images/vip-card1.png new file mode 100644 index 0000000000000000000000000000000000000000..4d483f81d5bcf87e88a948c8e7e09b4f4c7a1945 GIT binary patch literal 16988 zcmV(!K;^%QP);M1&0drDELIAGL9O(c600d`2O+f$vv5yPvt zTI*ZiT6^vN<4^wNPZB)gb+BLI=37T_v|nIxjDU3u5C$kX_{s@i9pHy_^Gflzy-0$p2{9Ail61QHH@9LS#pmIFEk{A6Kj06XjUGe$|8X z{@Q|KzX>Y8FV-AxhupI8xwVck6uYmUMBWZLlCKFP@wNFlzPKDO*Ri>ljcuem{iT zDLqD;F8$GlaC?h&hbpzU7SzAxuOjEje>B>-YFoJtWY?7c(A%kJ$h8N%kBl2}I|=z* zK6W)OVAx*5=FS?PdU~es|DxCI><1^uYk2vqM{xVy70hTCHt;+jw95?uQnBU$V|F-{##Mn{3jA7@)Oc6x*gAcq4&f8+TdyH;wt!qLiyy^kt1+I?urW5$1vtN??yLlj{JWxj%w z#Q_{YegONo9>KYbbGZKe0JwDuf2k6@`{p6M^aqasR~KNfJO@v0JP)6|{vv$n(hazL zeh=pSU-3brVivCbEBcRHvk!WjH<@tI#GwN{n3aYC#Q;j@N@;^e#RW?y)S{M8rhr8T z0rYQGxKOZS<|%>(zbB!IPY}Zthf+M+nOby!6>X{wy|klZ)&63oRC4=TERc^eV|3*K z7l5Or2o|4gqULacVpp^zg7A0_ZKj@GzBrd!^Js^dV~_Zb=jnnG9;NBKRB zaP;^TzIE#^y!O^DxK+Oej~~Aaul(r)xb*A-uH4vsvJ$-g`T_j$@7{wQcm{UxJK#q? z_*db_Klw@c`mJ~1H(&lHJXkEWlMrxO49d~1MvX}YsQRp9TNDwbV*#gb*(zAH0dd1F zZiSBJsw5bl)DOZ^i=%_7P@Fe7vY3(>9_Sb*T7?pYOK}CR4%ce#Xl*q(QjeelVu_;) zK&~_DIR&L}rj+Kw++Bbv^TogeN8s0g>BI0FZ~qSLuU>_luf4&SSise1 zHm6E(e6)nG|KV*0`>U)3-vR&Z`~M-l^WYx*(=Yujtmpv7a>TbA)mNEDnpU<~^TPr7 zVdaJZt|-v?_2_6^M+6zip>w@yU0_w$?h=Y| z95;wS7W=~#*!08TAJlsc-VWhrtWXa(;Sb-~gWdmo34Z*qe=q#h7ylOg+<*HGMatW6 zzX})5@4)8X*p%SwUwseia$8a0U;n^Qz%RV?yYTz>e;bzY07g7!AIN?LP0J}-qC%$B zTKm|Aj7=GgYz^&L=-@Mt!n|s5f>0LyM}&$)p6f1O%`~h zXap+|L(gLf#)?Wf&Ho3YkaLQXFrIn~=Bg(BEveJ46jjFs9i?KtL{UbzZX=6!%&crM zp*v+13B)aSU4?kG&*ilyM=c1?2{;DI2s+K6j2nEtU3k2{2mk3;@52{A{(1PvKl%gk zAAWHONAM6{d-)!G=VvddQp_GcIEGtqAF&H~9{%13e+2%|tA7Z;d+*m_f%~w6#|j;l z-=)xrAP5k3LTSk0!7Z&5Irpz=JcOVyDV+{_FlqBNQy~;5pg^rP z!tmuK5UpfTEx14g6hZ8SSPfl-GL)ISD)Y;Q{|;l2n{3QxZn3ImNtPoD5_f1(QdJZX zr7h}fP-QsKOxt$%^c`697_a!U+vNhR@xWaR*cd8zF!paRCq!pVOFT&lMPU1`bj;T} zVhmrwZ@lye@PQk<@B^Rx6#UPxy$-hz@4}<~HC((h!0fHpAMgpAe4~r-!rlw;um1Gk zG6LM;(;o53!X!rxJB+(hXS0pi`IxwtLu#*vhdS6MNE;joY&NKLON#I(97z}+DLjMo zGP}Qd!r_eC+4(hCQXo9JC8c=^d5ZhG!c=tO+Wo_gku&)~1|9}e43|8vT*>8o0Rak^ zSKThG?FtadcRU)ZgNLaVE^Sp;Wd#% z0(Vr%UVP575T@%Q2Xf8_(PF}w*6?jErc?7{5r?ZVxcvPibZo5Wvtlg;FYx%EI)yb6dH4xV>YNjl5@9JAdBZ@u$TN@{ zLqtK*iAtjir#jz&P)d0>i)g`($L&PIxKZ9SFx+3g4d?JOD*)pGE5f7oEx0oK0Nh=@ zt6WUON1}AMp_~DD@z;!_H&K9!YaeGb-cjBQPk=4>lhFQ;8(u+HF)@t6>Y_R zJ33@R(;Pm#_Zj%3gRj6s5#U%+pfVi2q$GU{jTTB9nU{XM$SNBxoek2Tt`^lPr&dyjfAJ9ee*7iMjr zn?j2+k-lGxGi7UB>HVr+vs$PZ_Y#Cqlo*|BR6U2HeUdLgcaa$PSf|3owSrMv$m&K# zV!5OK{fO?sh2b&_a#NM^4lBmB@*&RC?6~_VNxhUusBrCELxVMsz(QS}85QUltvP8l zvP)m#vAUTr|G~@fp=($9m~9x@|11|H%$Ccw+!m0D;r{AP4w*&xtQcs6oDFs@CW^#` zvuS|vUJVwUWedqlj1Y$G@K5f~%@;?E9fvx&OJc+viCJ`J7Duz`63lw$@JpSGX()w2 zi9(2;{exif44G@s1xOgCzGAd|g}4@F-2@@N^!3^v^HB{INYOxjezhQ3-b*q_AzT_H zxdm|p!p8?mjLj6j4@ctwcj+cv8J>Xyb_p~r;10b8AJ}}6RcLA9wvVqRj7%2DykdWK z2Oci(>gP+A-K@-YKqY}B3R&p{BnDXGWBB%iZ^PEf59@V=Xi^mf0fUO;g}BBOvtrR) z4F^pSTk|Q(lIQT3;laO--Y>%k7Fb_IzH5+wl*(thvqQb znDJ+KxgqNbF*IRWEIglY=`|wt45~muE^Y9~x@r*4NuLoSA}9&j5e|w)qLV3+l_j;n zLaxXdLgXtT!$z~dAQ_<6j-D;FWDUCCFY3qjcyUX0rFU3WBL)(cG#Y2ZJYSxIi^ElT zFutcPySIJ^o}YggytjOf#d!p;pZpn*)+Pk;rAlciqF}-)t}v&#P_DxL^?N#iOE#23 zGd;&psVJ(i84-{k!%Dq;aT8H6GvBfBbfZEt8cb?xmVf564Y;y*LoHh%v(|-&<70Xy zv9Rs!xj`!*Cro^6V@C&fW4@v1iogdlDBEDgQVXGCq9BVVa_pw0h6GjHW8YO(VuO7TZIPUp$?xjfxzS|od3WhU_3Mn)Ae^dQLw>6Z5`hxv z;OgIUj(j)TIp%JvDyQ^e9G80BSXZ5^TBmD$INpJi)d4&OHyFCjb-l6p6Zjw_zy{CP ztohuN@eoeehj6@n%^Vj@RTBIWFav2;i?_;=xY^lwpYN0jPq597hi zL2$B;d6=N%#c~by$6MO|Z=8GuUfB4IWkLyENk?;eWMS;5w*Ly8)C2fm4}SsPUcCYz zoP7*#%%6vwtG5j9g=-P#3vfQ+LLvvva5}-9xmue)M6;^!zwr{8tXQjeH!i^8;*l!J znv)uZT|$BAG{TB;CXy;!8yA(S60~^MEY3LvfrBw=CqMDb_i)&UHH_LQX|dLVyatF0f!s1-djgVESuI()eUkamX)zG#XbZ@mc03u<;Q;+lHlN6j5UK z5n5R3P0Oew5@V75)N`sU9Tm0o63NDEvFsw^EUt4@FBMZ4+t<6hSM(Qn| z;JYz@4zA5V!0zJ>R)lZCW#$xDXV1c|^_zyWZ2ge}4WjZpba zqKAMw@l%Z~DjjuS@ABqBZ%U&UNimlYiiT=or8LC|#WB4f_1FmJcSx=XMV+RG|PoBB-VQ?T+!9g4=1H($O z9_?>or`1EG8kQec8wnM1MWcXyn^x8K@Hvl9+21XXQn;3M8q-ef;(M$h%yxI>lq60)sV zejWduk2_jFgsrlp<1dPEe|%d>E;#H{Es6sA!?2GBuekq1c)-Z9ST9)k&uHK8 zE#78iIka)8)^0^*a!$f7yGmp*no7>hLPtw$_Adlih-PQ^In{XO+;zCa(~{Ca;Q%;n z!kp*HSL;(NkRXX3Ho3zqh=?KKBwy7mByoa;et4&|+zN+Bl2MzR4-}8d*Q{7O4&p2u zgS0!-jSo%7?andFt20q|gTORw>Oxx^KG4$jS< zf`jz~iw@9GC!!wX7Cs&BC?kK*!dF*tsg%e;bujJ&`QE|XaP{0(K4egrAcEiG5OB`W ze{!-+wpnI_O^!f6%h!}4t780xgpTCI=Q%j!Bx{F5Oi8E=^8rpdzKt2auvpJf=WR?@j$VlrP7ldtq^Bb~un4LReht^-?bJ{b z*7+U!Z;p>(j+<&&YbAJw((f!@hL3E27Ea3vyODhjQP)@@;*NE=&yXv6H5Y*=!(3+S zJ5%-)X6732L@uGc!YxLFOMBPg_Q6};`cX-nPTolT&xs`INS9g%^1-)RMaoz@BL>#` z@4WGO*xlUqYLc}=p=|3F9xv!hTHVs~-G zEPqdlosOTQ;-h*`=M1Xz8zJ?YM2fivj2Xxv<#w-()X1X)bb)8ZL>I=PJ4HR2|2{Z* z2p_)uBHTGZEjm3tJv79S*k67Rkl+zrso;ebqs9q-7Otszn+K%Q}4S_jB^b3s-8OL*y^Qjtq?` zOU&AAsnsX+>Kn=T;6&hgo2eX@Y!oxj(8&9hA1{hmlgyKs(ph3Hl+)GBuV?N+NNV72 z&Tx_ojl2jmA-Cy7Lg|uy((dL3O$tfef?9zLi~>8JMFi-fAlZe(bzO-%)B7#|95b|BaXy4Uuf}pQvJv8OrFb zWgZ=kfEqszSLo-2g{HKjyB9Q9tWnA+ncY){?=5z_vlh`PbXiTK>8L4et!)N)ay6OH z@Jb|k%_zR9Z2+;gr#rnV%k6`A;p*NqJl}Ar1*dwPLlLD0bseWDC@m=%4Yi<6$kZB| z8UiN34sdgf0Frgb^|L~#j~K8`9Rs$h>KqL5`{4C_}(_sK1VM-b%Iqo%I^!Y2FQX)8GSMYdspF_yE?ZPR1 z8`Sfh(R%v&$22y+`S_bUyR}@c;rWZ-#ms+;ef|Tucls{JuQXL22+Igx3sWc$|4Lmd zSO*Inf?K=IrG=}p8&sbKl>CG6gMw{lP-1DlrX+)z(j_+7N6%MSgDn@~1lo}>&B7s3g(n6hZI#3k#e5FGAF9<8h>H+zFuLH2MG2CN zkqAM}uQr32nF3OlH3z^z-ds6qgGT@ME(<&sUL<#LOKL*2k8VBwc4yh!Uf}1TU#t64|FyDpgqE@e;{w>Fb~{MVV@pG(Y(C;daiB{LgWoH- zK!g3#<5x!tAvuk8jG7Cn4Q*{wZFD89X$TN5GRdkD)G?A(6}|oevfq&i(<$w+qS2FD zK8CZjcx$gUTtg#=O6Z_3tO|@YMZF(53`WAns&cFZTMHbpodVbO8_P_pQvlTNi<)xgj5D zC5T?OZc5ie!x+o_P1GqaW`U~lw$^VOTx#&=u2%z1M}eF&cXY7mfm)dzctT6$u=Z@K zm;1?UW+y|KoC~xMZ!iZsG(CKErb4nmG(ik>mBb0Qv|s%nQ>zBI-j=-SGj;_!YeekO z#NH2xsSwRc0Q89;#fHhMB(Ll(&`ltDSvfT{3O_oi->u0LqL;1k)#f9L7qXx$mAz~> z)55r#upj<9(8OG8j2ziQ6^N*$c5N~a1RnQmTht9eawm0pPC`dj+R;KvRRVIqhhww7 zz&A|q+icj0O~TRS9LOPWdQlVB37I_I9yuXJ8zP8Wi&-pmw1jd=tF{$2WH;yqn{>7#I5~9K zR3?3-51PmDne|IwiDBptpJ#k>2*FV$d_($s{XMjLe{Wey_72d?HWW6OPO2oVapb5b zLnjMjgWXT8-3>!6Jld7Y8}ku?;AXAV%>h%_#);7=bN7UrPjHz_vCL$oj5Ws~|4HiJ zvz(I%Gms*pG^8#du`4MdB~kzmJMgDlEN1tZf{pydIEUTm(w zeK?wQcY-j%{M|8RKO+M;xSNP^YY^!(p%6SHrmmw8p#D56ssbR$rKCDN#sUE!C+8-E ziyEtk^)9zr3c%q=DtD!*(JF(h*Q)sikve_B_i1(rt+@^8tOOy-xckJ5DO`slIf;}h zCjB-DWFDxDM6&`#_ipnV(z(`FD=E$Z47HIx!-lBN<%;ENN;gf!52zK)@=&;*(a z3((J0CUFKztF0cH5%iEx3Ez?04MZ|@vb7*O9b9S}Av(lc($B`a+B~?=iP*THQvmBk z7L^3ck4BDK?xdlIrxcrUy+Wx*11kaeI8&B7_#U0ks@ohFB>%Na0Rz6j2GD zIW`8H(hWp-7w_{rXoyz=d7vcR>rqh=r!W-(MptYOX1b*zY@zUKim?I?Ln*MXtb-9inXm=c%Qx zzk;EIx&{mfgwT9`Y63LJ(y@i?c=J4o9{zVtk{SOxGU)d$S=4F+sX_FsLZt=`VAp=n@W;)On)E7uugawtTXbNtx<&9_`$CzV&JJa%O(4`G zDb*Bgaw=nvDX>r!otCa<2vKpYuEzJ^($JMm#-g@bYG#{S$O1I*gDrl4CUhc~v43Qv#G)FBg+>1GL zONV6fEs4}ZRkS7b9h$X;u3*9}v4RSdFW9<-DH-kkpq8XLUrC}HYZ^;;ET|{)73Qw) z)KJ*EM=jKIVTYroNhB0ty2@h_Vw&D-H*1?X%P1qGzBUu-J$85{%TG&<`mFBg0V`I7 zx$aVeTAF0T-B;W|C(Ygyr?{ch5)(M~5KS58*)^#XW}CHx=1qDB+1(|Wd>dTonud)= zq;=9X-zs2RhW3Aj@QF?&gMk zd`chUcI1FxvL0bhyKp$(OJ&mPD|aepUy&&(hLnl|vGtB2s#O+Y)cp<$`=i#UFBk(Z z!3pf^6y^r(G9nzCYw)!Klm!glKx~9i)v94#YP%u{)ea$B8r3@4%CMS*32L5zdCeU3 zaH8#SfG#7Z1Q5ITwR1V{k7K`}7<`3tpFGlYN<*HK>kXUQThXQkaG0v#uEr;B;EWrf zv70C8K97Dz9c0j)tcd_r1vrOS;m-1vv_>O+q=J=SV~nd3;gv5TAWua%gPiG**C-iX zStn7JHJ`!)4q*qczzN;cwQK`O8JKefY*E7m!c$I|a311m%k!J|gNIX@cuG%VJ;yGq z*NHiW5Q;UK1ISCa`}CQ@XrGkeFi$8<-JwVDm#OGB5?8iB&wM6)0& z3K3FMZGoxi?}lcbPU#Ws;x+ArEO-s8N1w3xh3+C;S#wWd-NK&ypl$~AF>9r^_i zX#}usuLHf3PP!dR9MipH8z*&j8*m<;hW+tP=t=asauptq@2G3qrVIRY(XSQhc87nI zDri5;Wj+Jv0=5FoR75zzoSlIrPec)9VZ>f}1`g>x#()KXx2Q#@R*DKk| z;ygy+I%XutF0CF_Lgoai3vaPmVb|`Mtb)XahzVU^oJ3M-6;~17DuMLPs|MVFOXYdE zJH7%@Mkk-`;nVPd6+q6}!KXNTyal)GpDF1Tk}pvn)g!%Gb;3RB%5NL~Sr9|$<7;&c zM@$%eu?LDG+-7lKW@OlB*KlGS0Zy4Xp0H2Zvf)d3i>W05=wLYHH^Ge%AU47GE+(L> zUsU2I2)PA~BL?($>fMl|%7i220nHj)X0Q2jCju^T?_KR{CR*F$ZtE_S7f5A3euN3L zpM;1RIOe%MxgBWE)_x6ck1x07b)3lCEEizEzO8ZNW;xG>z6p=urhYHW+IQhP++V+@ z%3?@DR?032>DAP;V6G9K8MCzaEPA%|b4n`KOc59G5H?xUPUt=?=n%HbMcvS1&HSNd z?&NZ9k|sy0iM@MKelTJ4&@9U?VVmOSWWiMm-JC;^62z{i}3%WKXk&>>Ab|4hK8;T@>)NveABip5` zaKFCU#*y2;H(b^YE#x)BP1s}?@Ce@2d(8{~T`$0WdRbjKbP+~oWxdTaW3Dxf!a^5J z6=zF-%o=r05(=yc$GVBdR(Xn*-~s=LQm`VxffFan0Z2mN*NrNm?1T^2gs(lpKN>mo zB##C8or^#h5a!_)3A9*0TG7uW=DieZa|L5pYY3A;G8Ad|386Q#2$06q^BccUrFsS} zT0|85B{)hNk1K<$IoPI4@POWe9zg~6AIaiX6gb~zi@(9gysqt;mo3<47x9SR_PZIk z(7+JU6JuVTEbgGC7`-E{O^LmcIf73O7FasjrL`#;fOWL>%jTe=if#OtTY{W-|yv1RO ztbQ4p6fPMBcFPSordz5Ay3LC5Oc$yR?5JDDoMw$2HvMSUikNO8LcIQkJ(TyKgS6F%z1ClA5B&nG1&u9iDc2hefEHegeB8kvG#7^0IA$Tjsu;6Z?OXw*! zm(fQr*a{RNP(r?w>Z5`!GZ_0k2a_eY{7CDn)v~oqbap}qkTo8B_iZMAva6Z2Ndm=P z5(TjOO*wuKp5eb=hrYC~co@ItgdeigkbNW8zEE}at%znBm`RdIasb+ymEp67ePT5W z&k?Ygt1E(TIJIC!xb2AWo)=j_M+J;rZZf11D}rpGWXM7dGCKu;AX!~})!G+&U^gk$ z*%3B4b=?Brf+U5kSFdpew_4{U{Vr#bPL4P~P(VFplep_(gZk)pgp}d;ZMj#=4CW+L zxP^=!oHsFS7utXmCfw#9tpa+7UGCAi@9mJV@EY8!-?I8#i5&1bxX%g^i(nM_Xr^kg z6yi>@Gfd5Pwn!b@RI(%yWP>D>ns%{jvi%9x&=Jo6V9qaHdSHQ>(}ylun?rzhb~V`U**)?~`eIM(&!V|XXX?Tcl=#>W^@jbi_4@3c| z)fsGXNGVgy3k}^JO~4drt$P*N)O|E^7FuiN%8&|Rnomin#HaI=1%G@_w?Y$3l*G{{ z=L%L5ce7wmIegrKtE_u>yc3w3E*Cu2Na86~qP1+RhL{7A!p9alO#HaJibmsX1S6+G zS$dYb1Fb|ovWK>h7xeC;HetrvizY0;8@2Z#v=PoQnMfz2M?}!l#o+=W+yI1=f=+Qm zF$rQbjSbv|1!wxug*%tI0eDAG7}&i^dicTmRSQFXY(xR%dv)LLo8|f&EImbfnrR)U z?iLfA04bW(zor>5Gra-}7QTSdCPUT&ALz$RMu|19bD+6p`%S0KgtkgJfPyRHdnq;LZx^bti1_c}MF zp?fFXXbZYJP^U`5?NA7zF)8z`eyBld~^O>qB>Fd0mEfA^m4~*MT>F4>{fHd5PJYh&Oq{xq72RF z?<)(PbI4M4N8N`i=uj?g2sb*`T%53iNVIE2^!>AhjSB!Yw(WzA&3!-!trq}v$N9Ce za~B3I)CCTdzt1=wm?W6oedib<_yX6W18zUQKy|PLRI6FA<7|bKT`!`A(v{99=s`mq z?R~e6k8%VS>WXK0vkx+dz~ia%5rp-{CssPPq1)Nnj%n80>ib6o(wETg2vw|l$U7QY zFft*<#)wO><>;GuIr3vFcqvMiv~tyHrhRq|Pb(=A1^Oudxa}b6uO&Rv$Li>+=7jeg zeuq(n_I#oi(oKZqgPc|!dktXseYe?!6^ynJaauvt_(KS#ba{-Ho4_2ja2xsCVm+`O zLTXx_&Qgx;?@F!sBfgJN)pZ-%UP_Q5Wabm7rI_cmqa;u^u@B6Vt4Ym)ZA)xQA8iAd zbwtvJFxWyiDJT&OoxUtm0M{akW0!QGH3wT9IfPh*{E7HC@sb{YjCYkIh;n=b{;OuK ztgPD@{gM?&CaDJ4jFE4q>uj0QMYO`mc!f4c+ZrM?x6%f#gJz1OD8J7&d;VF}?bU7? zmts>YUnU-lLu1p8TBp;#;jZ$A&cSJyp&1MjK<}h(JdsoR%x>oh z^{8>|3=F--&=M}uSi6zyj%=N?Ov+gFbDo`KsDkXe;yxOREzL*7NfFE;&Q3EFNT`%O=W^D$*-Mx=@&qLH(pobPys2N#Bf=sH3`B1HL=N0+_fHC7=oM(-}!Swoa9U zx}iWy7c)zQN^Q>5G@h4v=+U7$b-@3X3gqI7AeJn-Y>5z5Ny@RVbdf}p%r+h=B0R0( zqht=Ha!GRq`Vc78@KSd4v`Hl2wA$fH#}>k0``t^&4I*8XGlCs~%;i{q2sqaOBdkE( zH;Yqp~2Bjb%P{%F1#MHOm)`_HVdRaTyki*~* zUniQK>ewdR0FvXx_mp*pU{lFJP3tr~@g2Ma!X=Zf;Yu9=dS(vf9ATywF0U;uJh`vh zx>7&Ia|awrtnh$20w{&LzWS$Gn;%v8eoJ0+Fsv$>l#jH`w=!cci`t*DXD`T@Ra*m)ICJjw0IhtT=k_<1r9Adf6c1@6lD4BTRlzxbP9Bpo6YTLHiHm;x(MU?1fpR1AUF?NmGa|&?&Q5z>{JS0A+ZeJzJNufsmptH*s zwbcGv)FKwA741ibF^72E4fNq0`BGZlLSV&ng`b1#^qEFFp_`ZRYg6isu^PLxO&?nh z0dl-&Il8Xx4T0^c8JcefgL)y0mZ%t^!C?bDDPA;1I8_VZmU4BSOJGI7hs+r+Xtq&u z1jm&Tp;$S)Dgu9_i(nzh;v=0*A&jN*gWs~1dCBC+s98sB8N*=IojOtM+SmD(X}$IH>t_f!w4*p2`&PL0z}k90iCXtbF!Q zS0srda2OaUfME$T6RAT3PDiYO)F&t~RbgEhdh>v6QG{99%G{3H0JdIB znYwIFEqMgV8|?5*S6GKsBniTZ@I=@$*RVlPY2!wH!ii-E0MCx1`Aqa5@ojQ#(iHYN z7vu~A6>_60P1pJYz|KEofQEC9wwf2*<8&MX&DYxV5K7BW__nwWiToCE;b!Y?T&X#W zfTn>qnLC~6iIR?&5?dM}naQg#BU~>$v9Qhg{q!$BQeNsUp~v<;Xc|QH;=LfX>bol2*EyRi3Q2 zWQg0=Nh3RAjgoer+x!DoomjN(sDEXn@3U$eCd%QbyP&l@p2ti09P|X@#?dZ)^DK$Pu3)sZG|fFLh^e)a6`w_&i^()B-~)856U@F3)+S& z40-odFiW&>a1Q7BxC5IpJ%B|! z-YP^$?~pC6`2>^LAtl@oxq|EA@qq0K5ET3))Efdd~h+-WylJg!XoRCkL=1 zlc;Qp8UunBemFr-XbnL_Z_(YgqrBX_n6DrLnkb*-N2GdNU|toV?y5VGRZb#)Nuyg$ zCTb~JsdJvXf2jA^rRzFt8+jSItO{Emw1sUQhnz2R@CjE{}Y(PtP5w5?F`a z!mt%RSih}9!TO~jL&%>ffG}BsG3l>7ibWK|n0i;-;T9#@IIyCKi&)n~*e+LffKKqX z`ZOt4*@NeRe4F=R?g)L`K;cs+xQ@$ht8L_9>6lZ}QCcGKD21)!W~AZ?9P$zs(5!K) z9Ofcy%fH$gK*O|FrQD7WB}eEATl~BCAwa_-QvlAap2)KgK?+57bpMCg2t?`&WnATG z;R=#pS>PkR=3a(IIL2KFPcpVQRb)q|0qcc=cBy@$8q@Fzo!E+)`PRH?#uk05GGAX3Ye>K%psF ziVivj0BEIQluFTHAfgAFpaP@<^lqRi0vuN(dZGZrX~fvn$Y|yYARJ&*-GZ$8l5@8` z3Wx|!5JpH~g(hq{Zl)DP@ibX)M?t;5tx}fb#`eGs(6yC~wWf1RTOep#*+~?+Qh18Y zp%gF(M~Lj=S~H7V4la?6b9hzXy-?pK9N``Oau9xpC+G;E6POIcGA?ta*@rEL2S%fWM9%V`C@A|Okgou&eafMq`jId+r#Dt!is5Z>6hK#kb|x zZaVZwRS>$MokO0%gw%v=XNQ?NPX+0GTGmhyV5l34cDk~tZfn)G5IIec*Fj04L>Uq- z%#;LL&OjAF+ySMn*6f2 z_CU+v4EoSTPh;9bc6&UF>DOo+fPGs&380(ilusnjP<+n7<_-phLVflTSZZ40VVggg z*#Zg}a**{te2)`$3A^=K9R#Ul5>lvnb!UI#vPPZe-g9LH&tYts%L#Fjhm$}PM0irV zx9wrzgG8}CZP1BE7>$S(?Zd(s-v|Q83a+X=OWH9gLdwfZ_Kf5Tci5uK9wM$&-K31H z`cv!3-I(nq;ZHiF7$ek`%<=?vOmRj9R8vxLNj8ne$v8Lo@L1P`VF<;v*#n57-tf(xuwa+#Yla(2%eVRw#wbxw($G3PC=0CjK)%GT5qZl6ZVa zV|X1qnUJfdsQMg97OtT3O3;FT%L-$u>LyxeE3DpPJ$by4xL?5PyZ~|%Ok7MnPpLO& z$U>76&4$t`24rCZ{UwS}+R9+HczIBjK83@c#Vce-qfsK`2yS)p!tPCb?s0`yaaSQ8 z)D8Z4dQ}EZ$RK%Y_Fc=mIF1^0Q3^RCplyBAWh%2}G5y+4Z3bqg@2)F9W(9u)Kgai* z>)<9#^UopN(rtB)m_Z+*Da8W!A$WvXoMbxYBZrUWP>0nXNxxxk$(MUg+UTSX+zn<6 zy_`Lio4Rl_`Y@JJIf-0TQol>)Fgit_nZQHslZ=%f1_$r?jR3Yihac48Z*kg{E! zkBtyR6ZDueGX(I*5r$mB2yqy$q@uM*$QDrPcWpr`f^CKfeFx*ck4Y-ZsIEY3*<~RK zSjm`HA+eyO^RjS4t6v2sC5CnE$%r|f6J?sc*N^^#je$ea@z4i)C3nzJsH#h$l$wN4 zJ9t@_!^yJwEhSYC^LkbGW0Zks?x3)%Jz2DNc)R)n&!+zV54wA!Za|CFjLw*1S1_Fy7R)(WRU1cqRF0*Fj$OD42p zE5I8&6fMFw_z1a=L+XMbXQcU{U0YWNFMQy)^>Z!Z<5o(NOH#;f97xG^ zDnJT4va^1iE2bcthL~iLN<5k;7R^xfGV3Z^A3UZiE)tjs3Mbatq6W$@M}3OvSnX-3 zYcE%i8U#xjZ%8jYaRt_pT zCxd!XOSM{th%8Kb)@m{GuqtKJXgja2K@{PR~o;ZMNEvFXvMa&;y7{wdYrB%n%it@hX6%+&=R6Z1w0vN z72m6bd`e_+2S?7gC~`ZYC=KyvVR?ftIGO1t1_HLJ%~6p9(}IaUC4x9+F{%XtkEJ%{g1{VuXA3CAf^= z4R`5xKw{O{qCHpTLd{SkVGtT4Hps8a5o$-)&MFMLpe+kYNtz;&qutj!2Aix3f@LLG z8wQD5aJg1!wT(4Q5rr7dHFR1eyh@wI4eaQOsFDEJHi|Iiwq%UKB{}=Ij2!9ui3wTE z^m@QDbX=*qQ`s0?QoI@gFi|!VC>&{1XFM_PrRKBca|w3si|`_cy6@@~yt(FBU_Vpd zCDld6pF`0YLXQII#uwg z>#x2~K)$TkVrlAPW^7>*M#rE9Jl2BoCPh7-9n4sejySU3b5r;4yh;Xlp1k zQ+N5DTVH@L9sPUV7C;c@(xrvGXMvlBEsWw3lePBC<0!>V9Xc!t%Ekv?PtB{LE5LT%y3rT`YtUSXO zIrr{4PNCB=c(Dh9uV^CdVx({jysk}KW&bGT}NnJ zWEFLp-z-1xZS_5qB21^IkQC{73RL!MLK|a60G)@~hVR<=+wi#D=26+#+{A^8+pw|0 z9O2pLuEMuoeup*j5Pom6qiUzR z-)f~7F+B}A&;{Bvf~bwCD@M`W!x)D%l(kAH@IkldHnmNsOugpqzBn{%Zs3z8YROr^ zEC(kRPyQLEpwV4R%b%E1V0Qz{t_FdSiJ{m($Q8x6vC-!zDlz9T>fiz|Nfx7h`mAY3 z%O29A3xy(3MlT$hfo)#315cMvz-M=U5Ps>=zq36iShgQ|;ltX_+4bvJ;mXrHaQDs; zqrrRd<;8!5zq#?x;Wt-5%ekv>@&$#ctZXpDx4TJW2)Yw~sAgIcz%*410bIQsi0RBS zF*vpUY8MboqD(H?pcd#A$1R}gmcDEcV-MS?rL1E8L4#6)vxEw}i?sx*D{%LjHYdvy z<3Mv4V+dRi+jyTDn7fERfw#4N>lLvLa2xPi-zX3=T!1U6D1DU~)j2#r`vUxp3;ziI z$D^NN<9<(x;T8uf*REYLC6Ir<@P*IAul(|tVZGXid-OH<_3>xmM>c+nN#dLE_4O~q zgZesOEV5JteWGDHIpq0nqO?FD=eZL&?RpY3TG;P=nl|Y7i-+4ZL1A}|z6E9xZTV~{ zWBZ=`o}D#?1*6g1HuW7DMC*Dy4GppHw<{Ugvy=M#hqf0nTOG<9MSND--8D3HGPbyl z>vYPi`!Yi}{(5_ae#ckTC)jPmbF0CsYkPT z^Wuvy=-__!E8l?iTA1nyqryY@e~WK$vv+kKpv@|RTT4v~ZKSAEQ60mMI0dYA8a#pB z*lMOL{hk;dG-z$thm1CoywO;b@kleb%k@5d;!_`m z&wlok(7$H=A1}P{5x9Q+8vNexeFbjbysH_lsue>;OGFy+q1O4nb=i;(ObI$+L4!Pu zE1iMtu)=!hoJpv@va$ZN4=9a}S{%V&fCePpZ(p&@+N?28z2p_aw0vNJ#zd z^f@Ud3(vZ00000NkvXXu0mjfzQRxN literal 0 HcmV?d00001 diff --git a/images/vip-card2.png b/images/vip-card2.png new file mode 100644 index 0000000000000000000000000000000000000000..0ee8206a107005d86b7222f559d535f8fccfacc5 GIT binary patch literal 25081 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 literal 0 HcmV?d00001 diff --git a/images/vip-card3.png b/images/vip-card3.png new file mode 100644 index 0000000000000000000000000000000000000000..0effbd4759b270f171f1f695b6e8880857f5b1ba GIT binary patch literal 27042 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 Rfzbc}002ovPDHLkV1idF{_Ow& literal 0 HcmV?d00001 diff --git a/images/wallet-solid.png b/images/wallet-solid.png new file mode 100644 index 0000000000000000000000000000000000000000..4f90e4e9dfee4d7b0836a6c5215b3ee30a5b6a09 GIT binary patch literal 398 zcmV;90df9`P)cSBU}X!JA%IEy#3BHmC+GUe9+hPoz$iz4N-*p*!_BMMBW6=J@kq=KZxiL2~tuTEtS4ASz9Wl?C|X&4=Fp0E76wPT<=frNZCjp z(w6E}jGyxH)kR2xZoNd_)BV0xiMA9ob0H@5DJelpiZ3N { + 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 @@ + + + + + + 好友页面布局预览 + + + +